From a0a9ce4d3199fb3b0f39311e1c95f7fef6addf19 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Tue, 27 Jan 2026 04:04:30 +0530 Subject: [PATCH] Add amenities and size fields to room types --- .../controller/RoomAmenities.kt | 122 ++++++++++++++++++ .../trisolarisserver/controller/RoomTypes.kt | 39 +++++- .../controller/dto/RoomTypeDtos.kt | 20 ++- .../models/room/RoomAmenity.kt | 36 ++++++ .../trisolarisserver/models/room/RoomType.kt | 14 ++ .../trisolarisserver/repo/RoomAmenityRepo.kt | 13 ++ .../android/trisolarisserver/repo/RoomRepo.kt | 4 +- .../trisolarisserver/repo/RoomTypeRepo.kt | 2 +- 8 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/models/room/RoomAmenity.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/repo/RoomAmenityRepo.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt new file mode 100644 index 0000000..86f37d8 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt @@ -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 { + 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 + ) +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt index 97915ea..bd69b7a 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt @@ -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): MutableSet { + 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 ) } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt index 60c08cd..b31259c 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/RoomTypeDtos.kt @@ -7,7 +7,10 @@ data class RoomTypeUpsertRequest( val name: String, val baseOccupancy: Int? = null, val maxOccupancy: Int? = null, - val otaAliases: Set? = null + val sqFeet: Int? = null, + val bathroomSqFeet: Int? = null, + val otaAliases: Set? = null, + val amenityIds: Set? = null ) data class RoomTypeResponse( @@ -17,5 +20,18 @@ data class RoomTypeResponse( val name: String, val baseOccupancy: Int, val maxOccupancy: Int, - val otaAliases: Set + val sqFeet: Int?, + val bathroomSqFeet: Int?, + val otaAliases: Set, + val amenities: Set +) + +data class AmenityUpsertRequest( + val name: String +) + +data class AmenityResponse( + val id: UUID, + val propertyId: UUID, + val name: String ) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/room/RoomAmenity.kt b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomAmenity.kt new file mode 100644 index 0000000..f2a8385 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomAmenity.kt @@ -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() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/room/RoomType.kt b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomType.kt index a32ac20..b999f78 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/room/RoomType.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/room/RoomType.kt @@ -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 = 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 = mutableSetOf(), + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") val createdAt: OffsetDateTime = OffsetDateTime.now() ) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RoomAmenityRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RoomAmenityRepo.kt new file mode 100644 index 0000000..4dd5fc8 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RoomAmenityRepo.kt @@ -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 { + fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomAmenity? + fun findByPropertyIdOrderByName(propertyId: UUID): List + fun findByPropertyIdAndIdIn(propertyId: UUID, ids: Set): List + fun existsByPropertyIdAndName(propertyId: UUID, name: String): Boolean + fun existsByPropertyIdAndNameAndIdNot(propertyId: UUID, name: String, id: UUID): Boolean +} diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RoomRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RoomRepo.kt index 4d92812..0ecc700 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/RoomRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RoomRepo.kt @@ -10,10 +10,10 @@ import java.util.UUID interface RoomRepo : JpaRepository { - @EntityGraph(attributePaths = ["roomType", "roomType.otaAliases"]) + @EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"]) fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List - @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 diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/RoomTypeRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/RoomTypeRepo.kt index b99de4d..4a0d36b 100644 --- a/src/main/kotlin/com/android/trisolarisserver/repo/RoomTypeRepo.kt +++ b/src/main/kotlin/com/android/trisolarisserver/repo/RoomTypeRepo.kt @@ -8,7 +8,7 @@ import java.util.UUID interface RoomTypeRepo : JpaRepository { 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 fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean