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.RoomTypeResponse
|
||||||
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
|
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
|
||||||
import com.android.trisolarisserver.repo.PropertyRepo
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomAmenityRepo
|
||||||
import com.android.trisolarisserver.repo.RoomRepo
|
import com.android.trisolarisserver.repo.RoomRepo
|
||||||
import com.android.trisolarisserver.repo.RoomTypeRepo
|
import com.android.trisolarisserver.repo.RoomTypeRepo
|
||||||
import com.android.trisolarisserver.models.property.Role
|
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.models.room.RoomType
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
@@ -28,6 +30,7 @@ import java.util.UUID
|
|||||||
class RoomTypes(
|
class RoomTypes(
|
||||||
private val propertyAccess: PropertyAccess,
|
private val propertyAccess: PropertyAccess,
|
||||||
private val roomTypeRepo: RoomTypeRepo,
|
private val roomTypeRepo: RoomTypeRepo,
|
||||||
|
private val roomAmenityRepo: RoomAmenityRepo,
|
||||||
private val roomRepo: RoomRepo,
|
private val roomRepo: RoomRepo,
|
||||||
private val propertyRepo: PropertyRepo
|
private val propertyRepo: PropertyRepo
|
||||||
) {
|
) {
|
||||||
@@ -66,11 +69,27 @@ class RoomTypes(
|
|||||||
name = request.name,
|
name = request.name,
|
||||||
baseOccupancy = request.baseOccupancy ?: 2,
|
baseOccupancy = request.baseOccupancy ?: 2,
|
||||||
maxOccupancy = request.maxOccupancy ?: 3,
|
maxOccupancy = request.maxOccupancy ?: 3,
|
||||||
|
sqFeet = request.sqFeet,
|
||||||
|
bathroomSqFeet = request.bathroomSqFeet,
|
||||||
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
||||||
)
|
)
|
||||||
|
if (request.amenityIds != null) {
|
||||||
|
roomType.amenities = resolveAmenities(propertyId, request.amenityIds)
|
||||||
|
}
|
||||||
return roomTypeRepo.save(roomType).toResponse()
|
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}")
|
@PutMapping("/{roomTypeId}")
|
||||||
fun updateRoomType(
|
fun updateRoomType(
|
||||||
@PathVariable propertyId: UUID,
|
@PathVariable propertyId: UUID,
|
||||||
@@ -93,9 +112,14 @@ class RoomTypes(
|
|||||||
roomType.name = request.name
|
roomType.name = request.name
|
||||||
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
|
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
|
||||||
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
||||||
|
roomType.sqFeet = request.sqFeet ?: roomType.sqFeet
|
||||||
|
roomType.bathroomSqFeet = request.bathroomSqFeet ?: roomType.bathroomSqFeet
|
||||||
if (request.otaAliases != null) {
|
if (request.otaAliases != null) {
|
||||||
roomType.otaAliases = request.otaAliases.toMutableSet()
|
roomType.otaAliases = request.otaAliases.toMutableSet()
|
||||||
}
|
}
|
||||||
|
if (request.amenityIds != null) {
|
||||||
|
roomType.amenities = resolveAmenities(propertyId, request.amenityIds)
|
||||||
|
}
|
||||||
return roomTypeRepo.save(roomType).toResponse()
|
return roomTypeRepo.save(roomType).toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +160,19 @@ private fun RoomType.toResponse(): RoomTypeResponse {
|
|||||||
name = name,
|
name = name,
|
||||||
baseOccupancy = baseOccupancy,
|
baseOccupancy = baseOccupancy,
|
||||||
maxOccupancy = maxOccupancy,
|
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 name: String,
|
||||||
val baseOccupancy: Int? = null,
|
val baseOccupancy: Int? = null,
|
||||||
val maxOccupancy: 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(
|
data class RoomTypeResponse(
|
||||||
@@ -17,5 +20,18 @@ data class RoomTypeResponse(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val baseOccupancy: Int,
|
val baseOccupancy: Int,
|
||||||
val maxOccupancy: 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)
|
@Column(name = "max_occupancy", nullable = false)
|
||||||
var maxOccupancy: Int = 3,
|
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)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
@CollectionTable(
|
@CollectionTable(
|
||||||
name = "room_type_alias",
|
name = "room_type_alias",
|
||||||
@@ -40,6 +46,14 @@ class RoomType(
|
|||||||
@Column(name = "alias", nullable = false)
|
@Column(name = "alias", nullable = false)
|
||||||
var otaAliases: MutableSet<String> = mutableSetOf(),
|
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")
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
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> {
|
interface RoomRepo : JpaRepository<Room, UUID> {
|
||||||
|
|
||||||
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases"])
|
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"])
|
||||||
fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List<Room>
|
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 findByIdAndPropertyId(id: UUID, propertyId: UUID): Room?
|
||||||
|
|
||||||
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean
|
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
|||||||
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
|
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
|
||||||
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
|
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
|
||||||
fun findByPropertyIdAndCodeIgnoreCase(propertyId: UUID, code: String): RoomType?
|
fun findByPropertyIdAndCodeIgnoreCase(propertyId: UUID, code: String): RoomType?
|
||||||
@EntityGraph(attributePaths = ["property", "otaAliases"])
|
@EntityGraph(attributePaths = ["property", "otaAliases", "amenities"])
|
||||||
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
|
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
|
||||||
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
|
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
|
||||||
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean
|
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user