ai removed boilerplate
All checks were successful
build-and-deploy / build-deploy (push) Successful in 31s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 31s
This commit is contained in:
@@ -7,65 +7,30 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
@Component
|
||||
class GuestDocumentEvents(
|
||||
private val guestDocumentRepo: GuestDocumentRepo,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
private val emitters: MutableMap<GuestDocKey, CopyOnWriteArrayList<SseEmitter>> = ConcurrentHashMap()
|
||||
private val hub = SseHub<GuestDocKey>("guest-documents") { key ->
|
||||
buildSnapshot(key.propertyId, key.guestId)
|
||||
}
|
||||
|
||||
fun subscribe(propertyId: UUID, guestId: UUID): SseEmitter {
|
||||
val key = GuestDocKey(propertyId, guestId)
|
||||
val emitter = SseEmitter(0L)
|
||||
emitters.computeIfAbsent(key) { CopyOnWriteArrayList() }.add(emitter)
|
||||
emitter.onCompletion { emitters[key]?.remove(emitter) }
|
||||
emitter.onTimeout { emitters[key]?.remove(emitter) }
|
||||
emitter.onError { emitters[key]?.remove(emitter) }
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("guest-documents").data(buildSnapshot(propertyId, guestId)))
|
||||
} catch (_: IOException) {
|
||||
emitters[key]?.remove(emitter)
|
||||
}
|
||||
return emitter
|
||||
return hub.subscribe(key)
|
||||
}
|
||||
|
||||
fun emit(propertyId: UUID, guestId: UUID) {
|
||||
val key = GuestDocKey(propertyId, guestId)
|
||||
val list = emitters[key] ?: return
|
||||
val data = buildSnapshot(propertyId, guestId)
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("guest-documents").data(data))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
hub.emit(key)
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "25000")
|
||||
fun heartbeat() {
|
||||
emitters.forEach { (_, list) ->
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("ping").data("ok"))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
}
|
||||
hub.heartbeat()
|
||||
}
|
||||
|
||||
private fun buildSnapshot(propertyId: UUID, guestId: UUID): List<GuestDocumentResponse> {
|
||||
|
||||
@@ -7,63 +7,28 @@ import com.android.trisolarisserver.repo.RoomStayRepo
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
@Component
|
||||
class RoomBoardEvents(
|
||||
private val roomRepo: RoomRepo,
|
||||
private val roomStayRepo: RoomStayRepo
|
||||
) {
|
||||
private val emitters: MutableMap<UUID, CopyOnWriteArrayList<SseEmitter>> = ConcurrentHashMap()
|
||||
private val hub = SseHub<UUID>("room-board") { propertyId ->
|
||||
buildSnapshot(propertyId)
|
||||
}
|
||||
|
||||
fun subscribe(propertyId: UUID): SseEmitter {
|
||||
val emitter = SseEmitter(0L)
|
||||
emitters.computeIfAbsent(propertyId) { CopyOnWriteArrayList() }.add(emitter)
|
||||
emitter.onCompletion { emitters[propertyId]?.remove(emitter) }
|
||||
emitter.onTimeout { emitters[propertyId]?.remove(emitter) }
|
||||
emitter.onError { emitters[propertyId]?.remove(emitter) }
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("room-board").data(buildSnapshot(propertyId)))
|
||||
} catch (_: IOException) {
|
||||
emitters[propertyId]?.remove(emitter)
|
||||
}
|
||||
return emitter
|
||||
return hub.subscribe(propertyId)
|
||||
}
|
||||
|
||||
fun emit(propertyId: UUID) {
|
||||
val data = buildSnapshot(propertyId)
|
||||
val list = emitters[propertyId] ?: return
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("room-board").data(data))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
hub.emit(propertyId)
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "25000")
|
||||
fun heartbeat() {
|
||||
emitters.forEach { (_, list) ->
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("ping").data("ok"))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
}
|
||||
hub.heartbeat()
|
||||
}
|
||||
|
||||
private fun buildSnapshot(propertyId: UUID): List<RoomBoardResponse> {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.android.trisolarisserver.component
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class SseHub<K>(
|
||||
private val eventName: String,
|
||||
private val snapshot: (K) -> Any
|
||||
) {
|
||||
private val emitters: MutableMap<K, CopyOnWriteArrayList<SseEmitter>> = ConcurrentHashMap()
|
||||
|
||||
fun subscribe(key: K): SseEmitter {
|
||||
val emitter = SseEmitter(0L)
|
||||
emitters.computeIfAbsent(key) { CopyOnWriteArrayList() }.add(emitter)
|
||||
emitter.onCompletion { emitters[key]?.remove(emitter) }
|
||||
emitter.onTimeout { emitters[key]?.remove(emitter) }
|
||||
emitter.onError { emitters[key]?.remove(emitter) }
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name(eventName).data(snapshot(key)))
|
||||
} catch (_: IOException) {
|
||||
emitters[key]?.remove(emitter)
|
||||
}
|
||||
return emitter
|
||||
}
|
||||
|
||||
fun emit(key: K) {
|
||||
val list = emitters[key] ?: return
|
||||
val data = snapshot(key)
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name(eventName).data(data))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
fun heartbeat() {
|
||||
emitters.forEach { (_, list) ->
|
||||
val dead = mutableListOf<SseEmitter>()
|
||||
for (emitter in list) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("ping").data("ok"))
|
||||
} catch (_: IOException) {
|
||||
dead.add(emitter)
|
||||
}
|
||||
}
|
||||
if (dead.isNotEmpty()) {
|
||||
list.removeAll(dead.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@@ -38,7 +37,10 @@ class BookingBalances(
|
||||
if (booking.property.id != propertyId) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||
}
|
||||
val expected = computeExpectedPay(bookingId, booking.property.timezone)
|
||||
val expected = computeExpectedPay(
|
||||
roomStayRepo.findByBookingId(bookingId),
|
||||
booking.property.timezone
|
||||
)
|
||||
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||
val pending = expected - collected
|
||||
return BookingBalanceResponse(
|
||||
@@ -47,26 +49,4 @@ class BookingBalances(
|
||||
pending = pending
|
||||
)
|
||||
}
|
||||
|
||||
private fun computeExpectedPay(bookingId: UUID, timezone: String?): Long {
|
||||
val stays = roomStayRepo.findByBookingId(bookingId)
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private fun daysBetweenInclusive(start: LocalDate, end: LocalDate): Long {
|
||||
val diff = end.toEpochDay() - start.toEpochDay()
|
||||
return if (diff <= 0) 1L else diff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,47 +716,6 @@ class BookingFlow(
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeExpectedPay(stays: List<RoomStay>, timezone: String?): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private fun computeExpectedPayTotal(
|
||||
stays: List<RoomStay>,
|
||||
expectedCheckoutAt: OffsetDateTime?,
|
||||
timezone: String?
|
||||
): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: expectedCheckoutAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private fun daysBetweenInclusive(start: java.time.LocalDate, end: java.time.LocalDate): Long {
|
||||
val diff = end.toEpochDay() - start.toEpochDay()
|
||||
return if (diff <= 0) 1L else diff
|
||||
}
|
||||
|
||||
private fun isTransportModeAllowed(
|
||||
property: com.android.trisolarisserver.models.property.Property,
|
||||
mode: TransportMode
|
||||
|
||||
@@ -40,3 +40,11 @@ internal fun requireRole(
|
||||
propertyAccess.requireAnyRole(propertyId, resolved.userId, *roles)
|
||||
return resolved
|
||||
}
|
||||
|
||||
internal fun requireSuperAdmin(appUserRepo: AppUserRepo, principal: MyPrincipal?): AppUser {
|
||||
val user = requireUser(appUserRepo, principal)
|
||||
if (!user.superAdmin) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Super admin only")
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.android.trisolarisserver.repo.PropertyRepo
|
||||
import com.android.trisolarisserver.repo.RoomStayRepo
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.UUID
|
||||
@@ -75,6 +76,14 @@ internal fun parseOffset(value: String?): OffsetDateTime? {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseDate(value: String, errorMessage: String): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(value.trim())
|
||||
} catch (_: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun nowForProperty(timezone: String?): OffsetDateTime {
|
||||
val zone = try {
|
||||
if (timezone.isNullOrBlank()) ZoneId.of("Asia/Kolkata") else ZoneId.of(timezone)
|
||||
@@ -83,3 +92,44 @@ internal fun nowForProperty(timezone: String?): OffsetDateTime {
|
||||
}
|
||||
return OffsetDateTime.now(zone)
|
||||
}
|
||||
|
||||
internal fun computeExpectedPay(stays: List<RoomStay>, timezone: String?): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
internal fun computeExpectedPayTotal(
|
||||
stays: List<RoomStay>,
|
||||
expectedCheckoutAt: OffsetDateTime?,
|
||||
timezone: String?
|
||||
): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: expectedCheckoutAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
internal fun daysBetweenInclusive(start: LocalDate, end: LocalDate): Long {
|
||||
val diff = end.toEpochDay() - start.toEpochDay()
|
||||
return if (diff <= 0) 1L else diff
|
||||
}
|
||||
|
||||
@@ -214,25 +214,4 @@ class PayuPaymentLinksController(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun computeExpectedPay(stays: List<com.android.trisolarisserver.models.room.RoomStay>, timezone: String?): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private fun daysBetweenInclusive(start: java.time.LocalDate, end: java.time.LocalDate): Long {
|
||||
val diff = end.toEpochDay() - start.toEpochDay()
|
||||
return if (diff <= 0) 1L else diff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,24 +257,4 @@ class PayuQrPayments(
|
||||
return request.remoteAddr?.trim()?.ifBlank { null }
|
||||
}
|
||||
|
||||
private fun computeExpectedPay(stays: List<com.android.trisolarisserver.models.room.RoomStay>, timezone: String?): Long {
|
||||
if (stays.isEmpty()) return 0
|
||||
val now = nowForProperty(timezone)
|
||||
var total = 0L
|
||||
stays.forEach { stay ->
|
||||
val rate = stay.nightlyRate ?: 0L
|
||||
if (rate == 0L) return@forEach
|
||||
val start = stay.fromAt.toLocalDate()
|
||||
val endAt = stay.toAt ?: now
|
||||
val end = endAt.toLocalDate()
|
||||
val nights = daysBetweenInclusive(start, end)
|
||||
total += rate * nights
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private fun daysBetweenInclusive(start: java.time.LocalDate, end: java.time.LocalDate): Long {
|
||||
val diff = end.toEpochDay() - start.toEpochDay()
|
||||
return if (diff <= 0) 1L else diff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ class Properties(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: PropertyCreateRequest
|
||||
): PropertyResponse {
|
||||
val user = requireUser(principal)
|
||||
val user = requireUser(appUserRepo, principal)
|
||||
if (propertyRepo.existsByCode(request.code)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists")
|
||||
}
|
||||
@@ -80,7 +80,7 @@ class Properties(
|
||||
fun listProperties(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): List<PropertyResponse> {
|
||||
val user = requireUser(principal)
|
||||
val user = requireUser(appUserRepo, principal)
|
||||
return if (user.superAdmin) {
|
||||
propertyRepo.findAll().map { it.toResponse() }
|
||||
} else {
|
||||
@@ -211,21 +211,6 @@ class Properties(
|
||||
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 parseTransportModes(modes: Set<String>): MutableSet<TransportMode> {
|
||||
return try {
|
||||
modes.map { TransportMode.valueOf(it) }.toMutableSet()
|
||||
|
||||
@@ -132,8 +132,8 @@ class RatePlans(
|
||||
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||
val fromDate = parseDate(request.from)
|
||||
val toDate = parseDate(request.to)
|
||||
val fromDate = parseDate(request.from, "Invalid date")
|
||||
val toDate = parseDate(request.to, "Invalid date")
|
||||
if (toDate.isBefore(fromDate)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on/after from")
|
||||
}
|
||||
@@ -174,8 +174,8 @@ class RatePlans(
|
||||
requireMember(propertyAccess, propertyId, principal)
|
||||
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||
val fromDate = parseDate(from)
|
||||
val toDate = parseDate(to)
|
||||
val fromDate = parseDate(from, "Invalid date")
|
||||
val toDate = parseDate(to, "Invalid date")
|
||||
if (toDate.isBefore(fromDate)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "to must be on/after from")
|
||||
}
|
||||
@@ -209,20 +209,12 @@ class RatePlans(
|
||||
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||
val date = parseDate(rateDate)
|
||||
val date = parseDate(rateDate, "Invalid date")
|
||||
val existing = rateCalendarRepo.findByRatePlanIdAndRateDate(plan.id!!, date)
|
||||
?: return
|
||||
rateCalendarRepo.delete(existing)
|
||||
}
|
||||
|
||||
private fun parseDate(value: String): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(value.trim())
|
||||
} catch (_: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date")
|
||||
}
|
||||
}
|
||||
|
||||
private fun datesBetween(from: LocalDate, to: LocalDate): Sequence<LocalDate> {
|
||||
return generateSequence(from) { current ->
|
||||
val next = current.plusDays(1)
|
||||
|
||||
@@ -48,7 +48,7 @@ class RoomAmenities(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: AmenityUpsertRequest
|
||||
): AmenityResponse {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
|
||||
if (roomAmenityRepo.existsByName(request.name)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity already exists")
|
||||
@@ -68,7 +68,7 @@ class RoomAmenities(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: AmenityUpsertRequest
|
||||
): AmenityResponse {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
|
||||
val amenity = roomAmenityRepo.findById(amenityId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
|
||||
@@ -91,7 +91,7 @@ class RoomAmenities(
|
||||
@PathVariable amenityId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
) {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
|
||||
val amenity = roomAmenityRepo.findById(amenityId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
|
||||
@@ -106,24 +106,6 @@ class RoomAmenities(
|
||||
roomAmenityRepo.delete(amenity)
|
||||
}
|
||||
|
||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireSuperAdmin(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||
}
|
||||
if (!user.superAdmin) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Super admin only")
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateIconKey(iconKey: String?) {
|
||||
if (iconKey.isNullOrBlank()) return
|
||||
val file = Paths.get(pngRoot, "${iconKey}.png")
|
||||
|
||||
@@ -43,7 +43,7 @@ class RoomImageTags(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomImageTagUpsertRequest
|
||||
): RoomImageTagResponse {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
if (roomImageTagRepo.existsByName(request.name)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Tag already exists")
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class RoomImageTags(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: RoomImageTagUpsertRequest
|
||||
): RoomImageTagResponse {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
val tag = roomImageTagRepo.findById(tagId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Tag not found")
|
||||
if (roomImageTagRepo.existsByNameAndIdNot(request.name, tagId)) {
|
||||
@@ -74,30 +74,13 @@ class RoomImageTags(
|
||||
@PathVariable tagId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
) {
|
||||
requireSuperAdmin(principal)
|
||||
requireSuperAdmin(appUserRepo, principal)
|
||||
val tag = roomImageTagRepo.findById(tagId).orElse(null)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Tag not found")
|
||||
roomImageRepo.deleteTagLinks(tagId)
|
||||
roomImageTagRepo.delete(tag)
|
||||
}
|
||||
|
||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireSuperAdmin(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||
}
|
||||
if (!user.superAdmin) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Super admin only")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomImageTag.toResponse(): RoomImageTagResponse {
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@@ -69,7 +68,7 @@ class RoomTypes(
|
||||
}
|
||||
val roomType = roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, roomTypeCode)
|
||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
||||
val rateDate = parseDate(date)
|
||||
val rateDate = parseDate(date, "Invalid date")
|
||||
|
||||
if (!ratePlanCode.isNullOrBlank()) {
|
||||
val plan = ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
|
||||
@@ -198,19 +197,6 @@ class RoomTypes(
|
||||
roomTypeRepo.save(roomType)
|
||||
}
|
||||
|
||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(value: String): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(value.trim())
|
||||
} catch (_: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomType.toResponse(): RoomTypeResponse {
|
||||
|
||||
@@ -188,8 +188,8 @@ class Rooms(
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||
}
|
||||
|
||||
val fromDate = parseDate(from)
|
||||
val toDate = parseDate(to)
|
||||
val fromDate = parseDate(from, "Invalid date format")
|
||||
val toDate = parseDate(to, "Invalid date format")
|
||||
if (!toDate.isAfter(fromDate)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range")
|
||||
}
|
||||
@@ -226,8 +226,8 @@ class Rooms(
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||
}
|
||||
|
||||
val fromDate = parseDate(from)
|
||||
val toDate = parseDate(to)
|
||||
val fromDate = parseDate(from, "Invalid date format")
|
||||
val toDate = parseDate(to, "Invalid date format")
|
||||
if (!toDate.isAfter(fromDate)) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date range")
|
||||
}
|
||||
@@ -392,26 +392,12 @@ class Rooms(
|
||||
roomBoardEvents.emit(propertyId)
|
||||
}
|
||||
|
||||
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 parseDate(value: String): LocalDate {
|
||||
return try {
|
||||
LocalDate.parse(value.trim())
|
||||
} catch (_: Exception) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date format")
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectRatePlan(plans: List<RatePlan>?, ratePlanCode: String?): RatePlan? {
|
||||
if (plans.isNullOrEmpty()) return null
|
||||
if (!ratePlanCode.isNullOrBlank()) {
|
||||
|
||||
Reference in New Issue
Block a user