Add amenities and size fields to room types
All checks were successful
build-and-deploy / build-deploy (push) Successful in 27s

This commit is contained in:
androidlover5842
2026-01-27 04:04:30 +05:30
parent f9c31a4d59
commit a0a9ce4d31
8 changed files with 244 additions and 6 deletions

View File

@@ -0,0 +1,122 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.AmenityResponse
import com.android.trisolarisserver.controller.dto.AmenityUpsertRequest
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RoomAmenity
import com.android.trisolarisserver.repo.PropertyRepo
import com.android.trisolarisserver.repo.RoomAmenityRepo
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}/amenities")
class RoomAmenities(
private val propertyAccess: PropertyAccess,
private val roomAmenityRepo: RoomAmenityRepo,
private val propertyRepo: PropertyRepo
) {
@GetMapping
fun listAmenities(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): List<AmenityResponse> {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
return roomAmenityRepo.findByPropertyIdOrderByName(propertyId).map { it.toResponse() }
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createAmenity(
@PathVariable propertyId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: AmenityUpsertRequest
): AmenityResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
if (roomAmenityRepo.existsByPropertyIdAndName(propertyId, request.name)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity already exists for property")
}
val property = propertyRepo.findById(propertyId).orElseThrow {
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
}
val amenity = RoomAmenity(
property = property,
name = request.name
)
return roomAmenityRepo.save(amenity).toResponse()
}
@PutMapping("/{amenityId}")
fun updateAmenity(
@PathVariable propertyId: UUID,
@PathVariable amenityId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestBody request: AmenityUpsertRequest
): AmenityResponse {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val amenity = roomAmenityRepo.findByIdAndPropertyId(amenityId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
if (roomAmenityRepo.existsByPropertyIdAndNameAndIdNot(propertyId, request.name, amenityId)) {
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity already exists for property")
}
amenity.name = request.name
return roomAmenityRepo.save(amenity).toResponse()
}
@DeleteMapping("/{amenityId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteAmenity(
@PathVariable propertyId: UUID,
@PathVariable amenityId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
) {
requirePrincipal(principal)
propertyAccess.requireMember(propertyId, principal!!.userId)
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
val amenity = roomAmenityRepo.findByIdAndPropertyId(amenityId, propertyId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
roomAmenityRepo.delete(amenity)
}
private fun requirePrincipal(principal: MyPrincipal?) {
if (principal == null) {
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
}
}
}
private fun RoomAmenity.toResponse(): AmenityResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Amenity id missing")
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
return AmenityResponse(
id = id,
propertyId = propertyId,
name = name
)
}

View File

@@ -4,9 +4,11 @@ import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.controller.dto.RoomTypeResponse
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
import com.android.trisolarisserver.repo.PropertyRepo
import com.android.trisolarisserver.repo.RoomAmenityRepo
import com.android.trisolarisserver.repo.RoomRepo
import com.android.trisolarisserver.repo.RoomTypeRepo
import com.android.trisolarisserver.models.property.Role
import com.android.trisolarisserver.models.room.RoomAmenity
import com.android.trisolarisserver.models.room.RoomType
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.http.HttpStatus
@@ -28,6 +30,7 @@ import java.util.UUID
class RoomTypes(
private val propertyAccess: PropertyAccess,
private val roomTypeRepo: RoomTypeRepo,
private val roomAmenityRepo: RoomAmenityRepo,
private val roomRepo: RoomRepo,
private val propertyRepo: PropertyRepo
) {
@@ -66,11 +69,27 @@ class RoomTypes(
name = request.name,
baseOccupancy = request.baseOccupancy ?: 2,
maxOccupancy = request.maxOccupancy ?: 3,
sqFeet = request.sqFeet,
bathroomSqFeet = request.bathroomSqFeet,
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
)
if (request.amenityIds != null) {
roomType.amenities = resolveAmenities(propertyId, request.amenityIds)
}
return roomTypeRepo.save(roomType).toResponse()
}
private fun resolveAmenities(propertyId: UUID, ids: Set<UUID>): MutableSet<RoomAmenity> {
if (ids.isEmpty()) {
return mutableSetOf()
}
val amenities = roomAmenityRepo.findByPropertyIdAndIdIn(propertyId, ids)
if (amenities.size != ids.size) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
}
return amenities.toMutableSet()
}
@PutMapping("/{roomTypeId}")
fun updateRoomType(
@PathVariable propertyId: UUID,
@@ -93,9 +112,14 @@ class RoomTypes(
roomType.name = request.name
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
roomType.sqFeet = request.sqFeet ?: roomType.sqFeet
roomType.bathroomSqFeet = request.bathroomSqFeet ?: roomType.bathroomSqFeet
if (request.otaAliases != null) {
roomType.otaAliases = request.otaAliases.toMutableSet()
}
if (request.amenityIds != null) {
roomType.amenities = resolveAmenities(propertyId, request.amenityIds)
}
return roomTypeRepo.save(roomType).toResponse()
}
@@ -136,6 +160,19 @@ private fun RoomType.toResponse(): RoomTypeResponse {
name = name,
baseOccupancy = baseOccupancy,
maxOccupancy = maxOccupancy,
otaAliases = otaAliases.toSet()
sqFeet = sqFeet,
bathroomSqFeet = bathroomSqFeet,
otaAliases = otaAliases.toSet(),
amenities = amenities.map { it.toResponse() }.toSet()
)
}
private fun RoomAmenity.toResponse(): com.android.trisolarisserver.controller.dto.AmenityResponse {
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Amenity id missing")
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
return com.android.trisolarisserver.controller.dto.AmenityResponse(
id = id,
propertyId = propertyId,
name = name
)
}

View File

@@ -7,7 +7,10 @@ data class RoomTypeUpsertRequest(
val name: String,
val baseOccupancy: Int? = null,
val maxOccupancy: Int? = null,
val otaAliases: Set<String>? = null
val sqFeet: Int? = null,
val bathroomSqFeet: Int? = null,
val otaAliases: Set<String>? = null,
val amenityIds: Set<UUID>? = null
)
data class RoomTypeResponse(
@@ -17,5 +20,18 @@ data class RoomTypeResponse(
val name: String,
val baseOccupancy: Int,
val maxOccupancy: Int,
val otaAliases: Set<String>
val sqFeet: Int?,
val bathroomSqFeet: Int?,
val otaAliases: Set<String>,
val amenities: Set<AmenityResponse>
)
data class AmenityUpsertRequest(
val name: String
)
data class AmenityResponse(
val id: UUID,
val propertyId: UUID,
val name: String
)

View File

@@ -0,0 +1,36 @@
package com.android.trisolarisserver.models.room
import com.android.trisolarisserver.models.property.Property
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.time.OffsetDateTime
import java.util.UUID
@Entity
@Table(
name = "room_amenity",
uniqueConstraints = [UniqueConstraint(columnNames = ["property_id", "name"])]
)
class RoomAmenity(
@Id
@GeneratedValue
@Column(columnDefinition = "uuid")
val id: UUID? = null,
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "property_id", nullable = false)
var property: Property,
@Column(nullable = false)
var name: String,
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
val createdAt: OffsetDateTime = OffsetDateTime.now()
)

View File

@@ -32,6 +32,12 @@ class RoomType(
@Column(name = "max_occupancy", nullable = false)
var maxOccupancy: Int = 3,
@Column(name = "sq_feet")
var sqFeet: Int? = null,
@Column(name = "bathroom_sq_feet")
var bathroomSqFeet: Int? = null,
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(
name = "room_type_alias",
@@ -40,6 +46,14 @@ class RoomType(
@Column(name = "alias", nullable = false)
var otaAliases: MutableSet<String> = mutableSetOf(),
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "room_type_amenity_link",
joinColumns = [JoinColumn(name = "room_type_id")],
inverseJoinColumns = [JoinColumn(name = "amenity_id")]
)
var amenities: MutableSet<RoomAmenity> = mutableSetOf(),
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
val createdAt: OffsetDateTime = OffsetDateTime.now()
)

View File

@@ -0,0 +1,13 @@
package com.android.trisolarisserver.repo
import com.android.trisolarisserver.models.room.RoomAmenity
import org.springframework.data.jpa.repository.JpaRepository
import java.util.UUID
interface RoomAmenityRepo : JpaRepository<RoomAmenity, UUID> {
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomAmenity?
fun findByPropertyIdOrderByName(propertyId: UUID): List<RoomAmenity>
fun findByPropertyIdAndIdIn(propertyId: UUID, ids: Set<UUID>): List<RoomAmenity>
fun existsByPropertyIdAndName(propertyId: UUID, name: String): Boolean
fun existsByPropertyIdAndNameAndIdNot(propertyId: UUID, name: String, id: UUID): Boolean
}

View File

@@ -10,10 +10,10 @@ import java.util.UUID
interface RoomRepo : JpaRepository<Room, UUID> {
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases"])
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"])
fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List<Room>
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases"])
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"])
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): Room?
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean

View File

@@ -8,7 +8,7 @@ import java.util.UUID
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
fun findByPropertyIdAndCodeIgnoreCase(propertyId: UUID, code: String): RoomType?
@EntityGraph(attributePaths = ["property", "otaAliases"])
@EntityGraph(attributePaths = ["property", "otaAliases", "amenities"])
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean