Add amenities and size fields to room types
All checks were successful
build-and-deploy / build-deploy (push) Successful in 27s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 27s
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user