diff --git a/src/main/kotlin/com/android/trisolarisserver/component/GuestDocumentEvents.kt b/src/main/kotlin/com/android/trisolarisserver/component/GuestDocumentEvents.kt index e4ac737..5053053 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/GuestDocumentEvents.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/GuestDocumentEvents.kt @@ -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> = ConcurrentHashMap() + private val hub = SseHub("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() - 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() - 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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/component/RoomBoardEvents.kt b/src/main/kotlin/com/android/trisolarisserver/component/RoomBoardEvents.kt index 115790f..8fcec2a 100644 --- a/src/main/kotlin/com/android/trisolarisserver/component/RoomBoardEvents.kt +++ b/src/main/kotlin/com/android/trisolarisserver/component/RoomBoardEvents.kt @@ -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> = ConcurrentHashMap() + private val hub = SseHub("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() - 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() - 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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/component/SseHub.kt b/src/main/kotlin/com/android/trisolarisserver/component/SseHub.kt new file mode 100644 index 0000000..2422b1d --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/SseHub.kt @@ -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( + private val eventName: String, + private val snapshot: (K) -> Any +) { + private val emitters: MutableMap> = 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() + 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() + 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()) + } + } + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/BookingBalances.kt b/src/main/kotlin/com/android/trisolarisserver/controller/BookingBalances.kt index 039910e..8e002ad 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/BookingBalances.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/BookingBalances.kt @@ -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 - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt index 4502774..a34efa0 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/BookingFlow.kt @@ -716,47 +716,6 @@ class BookingFlow( } } - private fun computeExpectedPay(stays: List, 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, - 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 diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt index 9fc80f1..8d8df29 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerAccess.kt @@ -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 +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt index 36c4df6..cfb7eda 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/ControllerLookups.kt @@ -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, 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, + 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 +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt index b7e38c1..a30c940 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuPaymentLinksController.kt @@ -214,25 +214,4 @@ class PayuPaymentLinksController( } } - - private fun computeExpectedPay(stays: List, 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 - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt b/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt index fec4ca0..7666274 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/PayuQrPayments.kt @@ -257,24 +257,4 @@ class PayuQrPayments( return request.remoteAddr?.trim()?.ifBlank { null } } - private fun computeExpectedPay(stays: List, 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 - } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt index 384c547..dc09f35 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt @@ -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 { - 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): MutableSet { return try { modes.map { TransportMode.valueOf(it) }.toMutableSet() diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RatePlans.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RatePlans.kt index 48a624e..95ffe18 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RatePlans.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RatePlans.kt @@ -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 { return generateSequence(from) { current -> val next = current.plusDays(1) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt index 779eff6..9e63898 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomAmenities.kt @@ -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") diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImageTags.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImageTags.kt index 7e05639..d7356f2 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomImageTags.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomImageTags.kt @@ -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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt index ed4e6e6..76a38a5 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/RoomTypes.kt @@ -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 { diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt index ac7f24f..f6bdd16 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Rooms.kt @@ -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): 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?, ratePlanCode: String?): RatePlan? { if (plans.isNullOrEmpty()) return null if (!ratePlanCode.isNullOrBlank()) {