Add rate plans, room stay rates, and payments ledger
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s
This commit is contained in:
24
AGENTS.md
24
AGENTS.md
@@ -48,14 +48,16 @@ Security/Auth
|
|||||||
Domain entities
|
Domain entities
|
||||||
- Property: code, name, addressText, emailAddresses, otaAliases, allowedTransportModes.
|
- Property: code, name, addressText, emailAddresses, otaAliases, allowedTransportModes.
|
||||||
- AppUser (global, superAdmin), PropertyUser (roles per property).
|
- AppUser (global, superAdmin), PropertyUser (roles per property).
|
||||||
- RoomType: code/name/occupancy + otaAliases.
|
- RoomType: code/name/occupancy + otaAliases + defaultRate.
|
||||||
- Room: roomNumber, floor, hasNfc, active, maintenance, notes.
|
- Room: roomNumber, floor, hasNfc, active, maintenance, notes.
|
||||||
- Booking: status, expected check-in/out, emailAuditPdfUrl, transportMode.
|
- Booking: status, expected check-in/out, emailAuditPdfUrl, transportMode.
|
||||||
- Guest (property-scoped).
|
- Guest (property-scoped).
|
||||||
- RoomStay.
|
- RoomStay (rate fields stored on stay).
|
||||||
- RoomStayChange (idempotent room move).
|
- RoomStayChange (idempotent room move).
|
||||||
- IssuedCard (cardId, cardIndex, issuedAt, expiresAt, issuedBy, revokedAt).
|
- IssuedCard (cardId, cardIndex, issuedAt, expiresAt, issuedBy, revokedAt).
|
||||||
- PropertyCardCounter (per-property cardIndex counter).
|
- PropertyCardCounter (per-property cardIndex counter).
|
||||||
|
- RatePlan + RateCalendar.
|
||||||
|
- Payment (ledger).
|
||||||
- GuestDocument (files + AI-extracted json).
|
- GuestDocument (files + AI-extracted json).
|
||||||
- GuestVehicle (property-scoped vehicle numbers).
|
- GuestVehicle (property-scoped vehicle numbers).
|
||||||
- InboundEmail (audit PDF + raw EML, extracted json, status).
|
- InboundEmail (audit PDF + raw EML, extracted json, status).
|
||||||
@@ -88,6 +90,7 @@ Rooms / inventory
|
|||||||
Room types
|
Room types
|
||||||
- POST /properties/{propertyId}/room-types
|
- POST /properties/{propertyId}/room-types
|
||||||
- GET /properties/{propertyId}/room-types
|
- GET /properties/{propertyId}/room-types
|
||||||
|
- GET /properties/{propertyId}/room-types/{roomTypeCode}/rate?date=YYYY-MM-DD&ratePlanCode=optional
|
||||||
- PUT /properties/{propertyId}/room-types/{roomTypeId}
|
- PUT /properties/{propertyId}/room-types/{roomTypeId}
|
||||||
- DELETE /properties/{propertyId}/room-types/{roomTypeId}
|
- DELETE /properties/{propertyId}/room-types/{roomTypeId}
|
||||||
|
|
||||||
@@ -117,6 +120,23 @@ Guest APIs
|
|||||||
- /properties/{propertyId}/guests/search?phone=... or ?vehicleNumber=...
|
- /properties/{propertyId}/guests/search?phone=... or ?vehicleNumber=...
|
||||||
- /properties/{propertyId}/guests/{guestId}/vehicles (add vehicle)
|
- /properties/{propertyId}/guests/{guestId}/vehicles (add vehicle)
|
||||||
|
|
||||||
|
Room stays
|
||||||
|
- POST /properties/{propertyId}/room-stays/{roomStayId}/change-rate
|
||||||
|
|
||||||
|
Rate plans
|
||||||
|
- POST /properties/{propertyId}/rate-plans
|
||||||
|
- GET /properties/{propertyId}/rate-plans
|
||||||
|
- PUT /properties/{propertyId}/rate-plans/{ratePlanId}
|
||||||
|
- DELETE /properties/{propertyId}/rate-plans/{ratePlanId}
|
||||||
|
- POST /properties/{propertyId}/rate-plans/{ratePlanId}/calendar
|
||||||
|
- GET /properties/{propertyId}/rate-plans/{ratePlanId}/calendar?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||||
|
- DELETE /properties/{propertyId}/rate-plans/{ratePlanId}/calendar/{rateDate}
|
||||||
|
|
||||||
|
Payments
|
||||||
|
- POST /properties/{propertyId}/bookings/{bookingId}/payments
|
||||||
|
- GET /properties/{propertyId}/bookings/{bookingId}/payments
|
||||||
|
- GET /properties/{propertyId}/bookings/{bookingId}/balance
|
||||||
|
|
||||||
Guest documents
|
Guest documents
|
||||||
- /properties/{propertyId}/guests/{guestId}/documents (upload/list)
|
- /properties/{propertyId}/guests/{guestId}/documents (upload/list)
|
||||||
- /properties/{propertyId}/guests/{guestId}/documents/{documentId}/file
|
- /properties/{propertyId}/guests/{guestId}/documents/{documentId}/file
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
|
import com.android.trisolarisserver.controller.dto.BookingBalanceResponse
|
||||||
|
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||||
|
import com.android.trisolarisserver.repo.PaymentRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomStayRepo
|
||||||
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
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
|
||||||
|
@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/balance")
|
||||||
|
class BookingBalances(
|
||||||
|
private val propertyAccess: PropertyAccess,
|
||||||
|
private val bookingRepo: BookingRepo,
|
||||||
|
private val roomStayRepo: RoomStayRepo,
|
||||||
|
private val paymentRepo: PaymentRepo
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun getBalance(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable bookingId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): BookingBalanceResponse {
|
||||||
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
|
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
|
}
|
||||||
|
if (booking.property.id != propertyId) {
|
||||||
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||||
|
}
|
||||||
|
val expected = computeExpectedPay(bookingId, booking.property.timezone)
|
||||||
|
val collected = paymentRepo.sumAmountByBookingId(bookingId)
|
||||||
|
val pending = expected - collected
|
||||||
|
return BookingBalanceResponse(
|
||||||
|
expectedPay = expected,
|
||||||
|
amountCollected = collected,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import com.android.trisolarisserver.db.repo.BookingRepo
|
|||||||
import com.android.trisolarisserver.models.booking.BookingStatus
|
import com.android.trisolarisserver.models.booking.BookingStatus
|
||||||
import com.android.trisolarisserver.models.booking.TransportMode
|
import com.android.trisolarisserver.models.booking.TransportMode
|
||||||
import com.android.trisolarisserver.models.room.RoomStay
|
import com.android.trisolarisserver.models.room.RoomStay
|
||||||
|
import com.android.trisolarisserver.models.room.RateSource
|
||||||
import com.android.trisolarisserver.models.property.Role
|
import com.android.trisolarisserver.models.property.Role
|
||||||
import com.android.trisolarisserver.repo.AppUserRepo
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyRepo
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
@@ -142,6 +143,10 @@ class BookingFlow(
|
|||||||
room = room,
|
room = room,
|
||||||
fromAt = checkInAt,
|
fromAt = checkInAt,
|
||||||
toAt = null,
|
toAt = null,
|
||||||
|
rateSource = parseRateSource(request.rateSource),
|
||||||
|
nightlyRate = request.nightlyRate,
|
||||||
|
ratePlanCode = request.ratePlanCode,
|
||||||
|
currency = request.currency ?: booking.property.currency,
|
||||||
createdBy = actor
|
createdBy = actor
|
||||||
)
|
)
|
||||||
roomStayRepo.save(stay)
|
roomStayRepo.save(stay)
|
||||||
@@ -273,6 +278,10 @@ class BookingFlow(
|
|||||||
room = room,
|
room = room,
|
||||||
fromAt = fromAt,
|
fromAt = fromAt,
|
||||||
toAt = toAt,
|
toAt = toAt,
|
||||||
|
rateSource = parseRateSource(request.rateSource),
|
||||||
|
nightlyRate = request.nightlyRate,
|
||||||
|
ratePlanCode = request.ratePlanCode,
|
||||||
|
currency = request.currency ?: booking.property.currency,
|
||||||
createdBy = actor
|
createdBy = actor
|
||||||
)
|
)
|
||||||
roomStayRepo.save(stay)
|
roomStayRepo.save(stay)
|
||||||
@@ -303,6 +312,15 @@ class BookingFlow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseRateSource(value: String?): RateSource? {
|
||||||
|
if (value.isNullOrBlank()) return null
|
||||||
|
return try {
|
||||||
|
RateSource.valueOf(value.trim())
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown rate source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isTransportModeAllowed(
|
private fun isTransportModeAllowed(
|
||||||
property: com.android.trisolarisserver.models.property.Property,
|
property: com.android.trisolarisserver.models.property.Property,
|
||||||
mode: TransportMode
|
mode: TransportMode
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
|
import com.android.trisolarisserver.controller.dto.PaymentCreateRequest
|
||||||
|
import com.android.trisolarisserver.controller.dto.PaymentResponse
|
||||||
|
import com.android.trisolarisserver.db.repo.BookingRepo
|
||||||
|
import com.android.trisolarisserver.models.booking.Payment
|
||||||
|
import com.android.trisolarisserver.models.booking.PaymentMethod
|
||||||
|
import com.android.trisolarisserver.models.property.Role
|
||||||
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
|
import com.android.trisolarisserver.repo.PaymentRepo
|
||||||
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
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.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.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/properties/{propertyId}/bookings/{bookingId}/payments")
|
||||||
|
class Payments(
|
||||||
|
private val propertyAccess: PropertyAccess,
|
||||||
|
private val propertyRepo: PropertyRepo,
|
||||||
|
private val bookingRepo: BookingRepo,
|
||||||
|
private val paymentRepo: PaymentRepo,
|
||||||
|
private val appUserRepo: AppUserRepo
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
fun create(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable bookingId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: PaymentCreateRequest
|
||||||
|
): PaymentResponse {
|
||||||
|
val actor = requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER, Role.STAFF)
|
||||||
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
|
}
|
||||||
|
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
|
}
|
||||||
|
if (booking.property.id != propertyId) {
|
||||||
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||||
|
}
|
||||||
|
if (request.amount <= 0) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "amount must be > 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
val method = parseMethod(request.method)
|
||||||
|
val receivedAt = parseOffset(request.receivedAt) ?: OffsetDateTime.now()
|
||||||
|
val payment = Payment(
|
||||||
|
property = property,
|
||||||
|
booking = booking,
|
||||||
|
amount = request.amount,
|
||||||
|
currency = request.currency ?: property.currency,
|
||||||
|
method = method,
|
||||||
|
reference = request.reference?.trim()?.ifBlank { null },
|
||||||
|
notes = request.notes?.trim()?.ifBlank { null },
|
||||||
|
receivedAt = receivedAt,
|
||||||
|
receivedBy = appUserRepo.findById(actor.userId).orElse(null)
|
||||||
|
)
|
||||||
|
return paymentRepo.save(payment).toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun list(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable bookingId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): List<PaymentResponse> {
|
||||||
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
|
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
|
}
|
||||||
|
if (booking.property.id != propertyId) {
|
||||||
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found for property")
|
||||||
|
}
|
||||||
|
return paymentRepo.findByBookingIdOrderByReceivedAtDesc(bookingId).map { it.toResponse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseMethod(value: String): PaymentMethod {
|
||||||
|
return try {
|
||||||
|
PaymentMethod.valueOf(value.trim())
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown payment method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Payment.toResponse(): PaymentResponse {
|
||||||
|
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Payment id missing")
|
||||||
|
val bookingId = booking.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Booking id missing")
|
||||||
|
return PaymentResponse(
|
||||||
|
id = id,
|
||||||
|
bookingId = bookingId,
|
||||||
|
amount = amount,
|
||||||
|
currency = currency,
|
||||||
|
method = method.name,
|
||||||
|
reference = reference,
|
||||||
|
notes = notes,
|
||||||
|
receivedAt = receivedAt.toString(),
|
||||||
|
receivedByUserId = receivedBy?.id
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
|
import com.android.trisolarisserver.controller.dto.RateCalendarResponse
|
||||||
|
import com.android.trisolarisserver.controller.dto.RateCalendarUpsertRequest
|
||||||
|
import com.android.trisolarisserver.controller.dto.RatePlanCreateRequest
|
||||||
|
import com.android.trisolarisserver.controller.dto.RatePlanResponse
|
||||||
|
import com.android.trisolarisserver.controller.dto.RatePlanUpdateRequest
|
||||||
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
|
import com.android.trisolarisserver.repo.RateCalendarRepo
|
||||||
|
import com.android.trisolarisserver.repo.RatePlanRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomTypeRepo
|
||||||
|
import com.android.trisolarisserver.models.property.Role
|
||||||
|
import com.android.trisolarisserver.models.room.RateCalendar
|
||||||
|
import com.android.trisolarisserver.models.room.RatePlan
|
||||||
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
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.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.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/properties/{propertyId}/rate-plans")
|
||||||
|
class RatePlans(
|
||||||
|
private val propertyAccess: PropertyAccess,
|
||||||
|
private val propertyRepo: PropertyRepo,
|
||||||
|
private val roomTypeRepo: RoomTypeRepo,
|
||||||
|
private val ratePlanRepo: RatePlanRepo,
|
||||||
|
private val rateCalendarRepo: RateCalendarRepo
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
fun create(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: RatePlanCreateRequest
|
||||||
|
): RatePlanResponse {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
if (ratePlanRepo.existsByPropertyIdAndCode(propertyId, request.code.trim())) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Rate plan code already exists")
|
||||||
|
}
|
||||||
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
|
}
|
||||||
|
val roomType = roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, request.roomTypeCode)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
||||||
|
|
||||||
|
val plan = RatePlan(
|
||||||
|
property = property,
|
||||||
|
roomType = roomType,
|
||||||
|
code = request.code.trim(),
|
||||||
|
name = request.name.trim(),
|
||||||
|
baseRate = request.baseRate,
|
||||||
|
currency = request.currency ?: property.currency,
|
||||||
|
updatedAt = OffsetDateTime.now()
|
||||||
|
)
|
||||||
|
return ratePlanRepo.save(plan).toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun list(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestParam(required = false) roomTypeCode: String?
|
||||||
|
): List<RatePlanResponse> {
|
||||||
|
requireMember(propertyAccess, propertyId, principal)
|
||||||
|
val plans = if (roomTypeCode.isNullOrBlank()) {
|
||||||
|
ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
|
||||||
|
} else {
|
||||||
|
ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
|
||||||
|
.filter { it.roomType.code.equals(roomTypeCode, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
return plans.map { it.toResponse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{ratePlanId}")
|
||||||
|
fun update(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable ratePlanId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: RatePlanUpdateRequest
|
||||||
|
): RatePlanResponse {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||||
|
plan.name = request.name.trim()
|
||||||
|
plan.baseRate = request.baseRate
|
||||||
|
plan.currency = request.currency ?: plan.currency
|
||||||
|
plan.updatedAt = OffsetDateTime.now()
|
||||||
|
return ratePlanRepo.save(plan).toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{ratePlanId}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
@Transactional
|
||||||
|
fun delete(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable ratePlanId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
) {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||||
|
rateCalendarRepo.findByRatePlanId(plan.id!!).forEach { rateCalendarRepo.delete(it) }
|
||||||
|
ratePlanRepo.delete(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{ratePlanId}/calendar")
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
@Transactional
|
||||||
|
fun upsertCalendar(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable ratePlanId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody requests: List<RateCalendarUpsertRequest>
|
||||||
|
): List<RateCalendarResponse> {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
val plan = ratePlanRepo.findByIdAndPropertyId(ratePlanId, propertyId)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||||
|
if (requests.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val updates = requests.map { req ->
|
||||||
|
val date = parseDate(req.rateDate)
|
||||||
|
val existing = rateCalendarRepo.findByRatePlanIdAndRateDate(plan.id!!, date)
|
||||||
|
if (existing != null) {
|
||||||
|
existing.rate = req.rate
|
||||||
|
existing
|
||||||
|
} else {
|
||||||
|
RateCalendar(
|
||||||
|
property = plan.property,
|
||||||
|
ratePlan = plan,
|
||||||
|
rateDate = date,
|
||||||
|
rate = req.rate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rateCalendarRepo.saveAll(updates).map { it.toResponse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{ratePlanId}/calendar")
|
||||||
|
fun listCalendar(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable ratePlanId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestParam from: String,
|
||||||
|
@RequestParam to: String
|
||||||
|
): List<RateCalendarResponse> {
|
||||||
|
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 items = rateCalendarRepo.findByRatePlanIdAndRateDateBetweenOrderByRateDateAsc(plan.id!!, fromDate, toDate)
|
||||||
|
return items.map { it.toResponse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{ratePlanId}/calendar/{rateDate}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
fun deleteCalendar(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable ratePlanId: UUID,
|
||||||
|
@PathVariable rateDate: String,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
) {
|
||||||
|
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 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 RatePlan.toResponse(): RatePlanResponse {
|
||||||
|
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate plan id missing")
|
||||||
|
val propertyId = property.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
|
||||||
|
val roomTypeId = roomType.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Room type id missing")
|
||||||
|
return RatePlanResponse(
|
||||||
|
id = id,
|
||||||
|
propertyId = propertyId,
|
||||||
|
roomTypeId = roomTypeId,
|
||||||
|
roomTypeCode = roomType.code,
|
||||||
|
code = code,
|
||||||
|
name = name,
|
||||||
|
baseRate = baseRate,
|
||||||
|
currency = currency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RateCalendar.toResponse(): RateCalendarResponse {
|
||||||
|
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate calendar id missing")
|
||||||
|
val planId = ratePlan.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Rate plan id missing")
|
||||||
|
return RateCalendarResponse(
|
||||||
|
id = id,
|
||||||
|
ratePlanId = planId,
|
||||||
|
rateDate = rateDate,
|
||||||
|
rate = rate
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -86,6 +86,10 @@ class RoomStayFlow(
|
|||||||
room = newRoom,
|
room = newRoom,
|
||||||
fromAt = movedAt,
|
fromAt = movedAt,
|
||||||
toAt = null,
|
toAt = null,
|
||||||
|
rateSource = stay.rateSource,
|
||||||
|
nightlyRate = stay.nightlyRate,
|
||||||
|
ratePlanCode = stay.ratePlanCode,
|
||||||
|
currency = stay.currency,
|
||||||
createdBy = actor
|
createdBy = actor
|
||||||
)
|
)
|
||||||
val savedNewStay = roomStayRepo.save(newStay)
|
val savedNewStay = roomStayRepo.save(newStay)
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package com.android.trisolarisserver.controller
|
|||||||
|
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
import com.android.trisolarisserver.controller.dto.ActiveRoomStayResponse
|
import com.android.trisolarisserver.controller.dto.ActiveRoomStayResponse
|
||||||
|
import com.android.trisolarisserver.controller.dto.RoomStayRateChangeRequest
|
||||||
|
import com.android.trisolarisserver.controller.dto.RoomStayRateChangeResponse
|
||||||
import com.android.trisolarisserver.models.property.Role
|
import com.android.trisolarisserver.models.property.Role
|
||||||
|
import com.android.trisolarisserver.models.room.RateSource
|
||||||
|
import com.android.trisolarisserver.models.room.RoomStay
|
||||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
import com.android.trisolarisserver.repo.RoomStayRepo
|
import com.android.trisolarisserver.repo.RoomStayRepo
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
@@ -10,8 +14,11 @@ import org.springframework.http.HttpStatus
|
|||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -57,6 +64,60 @@ class RoomStays(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/properties/{propertyId}/room-stays/{roomStayId}/change-rate")
|
||||||
|
fun changeRate(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable roomStayId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: RoomStayRateChangeRequest
|
||||||
|
): RoomStayRateChangeResponse {
|
||||||
|
requireRole(propertyAccess, propertyId, principal, Role.ADMIN, Role.MANAGER)
|
||||||
|
val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId)
|
||||||
|
|
||||||
|
val effectiveAt = parseOffset(request.effectiveAt)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "effectiveAt required")
|
||||||
|
if (!effectiveAt.isAfter(stay.fromAt)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "effectiveAt must be after fromAt")
|
||||||
|
}
|
||||||
|
if (stay.toAt != null && !effectiveAt.isBefore(stay.toAt)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "effectiveAt outside stay range")
|
||||||
|
}
|
||||||
|
|
||||||
|
val newStay = splitStay(stay, effectiveAt, request)
|
||||||
|
roomStayRepo.save(stay)
|
||||||
|
roomStayRepo.save(newStay)
|
||||||
|
return RoomStayRateChangeResponse(
|
||||||
|
oldRoomStayId = stay.id!!,
|
||||||
|
newRoomStayId = newStay.id!!,
|
||||||
|
effectiveAt = effectiveAt.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun splitStay(stay: RoomStay, effectiveAt: OffsetDateTime, request: RoomStayRateChangeRequest): RoomStay {
|
||||||
|
val oldToAt = stay.toAt
|
||||||
|
stay.toAt = effectiveAt
|
||||||
|
return RoomStay(
|
||||||
|
property = stay.property,
|
||||||
|
booking = stay.booking,
|
||||||
|
room = stay.room,
|
||||||
|
fromAt = effectiveAt,
|
||||||
|
toAt = oldToAt,
|
||||||
|
rateSource = parseRateSource(request.rateSource),
|
||||||
|
nightlyRate = request.nightlyRate,
|
||||||
|
ratePlanCode = request.ratePlanCode,
|
||||||
|
currency = request.currency ?: stay.property.currency,
|
||||||
|
createdBy = stay.createdBy
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRateSource(value: String): RateSource {
|
||||||
|
return try {
|
||||||
|
RateSource.valueOf(value.trim())
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown rate source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isAgentOnly(roles: Set<Role>): Boolean {
|
private fun isAgentOnly(roles: Set<Role>): Boolean {
|
||||||
if (!roles.contains(Role.AGENT)) return false
|
if (!roles.contains(Role.AGENT)) return false
|
||||||
val privileged = setOf(
|
val privileged = setOf(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.android.trisolarisserver.controller
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
import com.android.trisolarisserver.component.PropertyAccess
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
|
import com.android.trisolarisserver.controller.dto.RateResolveResponse
|
||||||
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.RateCalendarRepo
|
||||||
|
import com.android.trisolarisserver.repo.RatePlanRepo
|
||||||
import com.android.trisolarisserver.repo.RoomAmenityRepo
|
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
|
||||||
@@ -20,9 +23,11 @@ import org.springframework.web.bind.annotation.PostMapping
|
|||||||
import org.springframework.web.bind.annotation.PutMapping
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.time.LocalDate
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -32,7 +37,9 @@ class RoomTypes(
|
|||||||
private val roomTypeRepo: RoomTypeRepo,
|
private val roomTypeRepo: RoomTypeRepo,
|
||||||
private val roomAmenityRepo: RoomAmenityRepo,
|
private val roomAmenityRepo: RoomAmenityRepo,
|
||||||
private val roomRepo: RoomRepo,
|
private val roomRepo: RoomRepo,
|
||||||
private val propertyRepo: PropertyRepo
|
private val propertyRepo: PropertyRepo,
|
||||||
|
private val ratePlanRepo: RatePlanRepo,
|
||||||
|
private val rateCalendarRepo: RateCalendarRepo
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -46,6 +53,52 @@ class RoomTypes(
|
|||||||
return roomTypeRepo.findByPropertyIdOrderByCode(propertyId).map { it.toResponse() }
|
return roomTypeRepo.findByPropertyIdOrderByCode(propertyId).map { it.toResponse() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{roomTypeCode}/rate")
|
||||||
|
fun resolveRate(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@PathVariable roomTypeCode: String,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestParam date: String,
|
||||||
|
@RequestParam(required = false) ratePlanCode: String?
|
||||||
|
): RateResolveResponse {
|
||||||
|
if (principal != null) {
|
||||||
|
propertyAccess.requireMember(propertyId, principal.userId)
|
||||||
|
}
|
||||||
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
|
}
|
||||||
|
val roomType = roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, roomTypeCode)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
||||||
|
val rateDate = parseDate(date)
|
||||||
|
|
||||||
|
if (!ratePlanCode.isNullOrBlank()) {
|
||||||
|
val plan = ratePlanRepo.findByPropertyIdOrderByCode(propertyId)
|
||||||
|
.firstOrNull { it.code.equals(ratePlanCode, ignoreCase = true) }
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Rate plan not found")
|
||||||
|
if (plan.roomType.id != roomType.id) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Rate plan not for room type")
|
||||||
|
}
|
||||||
|
val override = rateCalendarRepo.findByRatePlanIdAndRateDate(plan.id!!, rateDate)
|
||||||
|
val rate = override?.rate ?: plan.baseRate
|
||||||
|
return RateResolveResponse(
|
||||||
|
roomTypeCode = roomType.code,
|
||||||
|
rateDate = rateDate,
|
||||||
|
rate = rate,
|
||||||
|
currency = plan.currency,
|
||||||
|
ratePlanCode = plan.code
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rate = roomType.defaultRate
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Default rate not set")
|
||||||
|
return RateResolveResponse(
|
||||||
|
roomTypeCode = roomType.code,
|
||||||
|
rateDate = rateDate,
|
||||||
|
rate = rate,
|
||||||
|
currency = property.currency
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun createRoomType(
|
fun createRoomType(
|
||||||
@@ -72,6 +125,7 @@ class RoomTypes(
|
|||||||
maxOccupancy = request.maxOccupancy ?: 3,
|
maxOccupancy = request.maxOccupancy ?: 3,
|
||||||
sqFeet = request.sqFeet,
|
sqFeet = request.sqFeet,
|
||||||
bathroomSqFeet = request.bathroomSqFeet,
|
bathroomSqFeet = request.bathroomSqFeet,
|
||||||
|
defaultRate = request.defaultRate,
|
||||||
active = request.active ?: true,
|
active = request.active ?: true,
|
||||||
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
||||||
)
|
)
|
||||||
@@ -116,6 +170,7 @@ class RoomTypes(
|
|||||||
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
||||||
roomType.sqFeet = request.sqFeet ?: roomType.sqFeet
|
roomType.sqFeet = request.sqFeet ?: roomType.sqFeet
|
||||||
roomType.bathroomSqFeet = request.bathroomSqFeet ?: roomType.bathroomSqFeet
|
roomType.bathroomSqFeet = request.bathroomSqFeet ?: roomType.bathroomSqFeet
|
||||||
|
roomType.defaultRate = request.defaultRate ?: roomType.defaultRate
|
||||||
roomType.active = request.active ?: roomType.active
|
roomType.active = request.active ?: roomType.active
|
||||||
if (request.otaAliases != null) {
|
if (request.otaAliases != null) {
|
||||||
roomType.otaAliases = request.otaAliases.toMutableSet()
|
roomType.otaAliases = request.otaAliases.toMutableSet()
|
||||||
@@ -148,6 +203,14 @@ class RoomTypes(
|
|||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
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 {
|
private fun RoomType.toResponse(): RoomTypeResponse {
|
||||||
@@ -162,6 +225,7 @@ private fun RoomType.toResponse(): RoomTypeResponse {
|
|||||||
maxOccupancy = maxOccupancy,
|
maxOccupancy = maxOccupancy,
|
||||||
sqFeet = sqFeet,
|
sqFeet = sqFeet,
|
||||||
bathroomSqFeet = bathroomSqFeet,
|
bathroomSqFeet = bathroomSqFeet,
|
||||||
|
defaultRate = defaultRate,
|
||||||
active = active,
|
active = active,
|
||||||
otaAliases = otaAliases.toSet(),
|
otaAliases = otaAliases.toSet(),
|
||||||
amenities = amenities.map { it.toResponse() }.toSet()
|
amenities = amenities.map { it.toResponse() }.toSet()
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ data class BookingCheckInRequest(
|
|||||||
val roomIds: List<UUID>,
|
val roomIds: List<UUID>,
|
||||||
val checkInAt: String? = null,
|
val checkInAt: String? = null,
|
||||||
val transportMode: String? = null,
|
val transportMode: String? = null,
|
||||||
|
val nightlyRate: Long? = null,
|
||||||
|
val rateSource: String? = null,
|
||||||
|
val ratePlanCode: String? = null,
|
||||||
|
val currency: String? = null,
|
||||||
val notes: String? = null
|
val notes: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,6 +64,10 @@ data class RoomStayPreAssignRequest(
|
|||||||
val roomId: UUID,
|
val roomId: UUID,
|
||||||
val fromAt: String,
|
val fromAt: String,
|
||||||
val toAt: String,
|
val toAt: String,
|
||||||
|
val nightlyRate: Long? = null,
|
||||||
|
val rateSource: String? = null,
|
||||||
|
val ratePlanCode: String? = null,
|
||||||
|
val currency: String? = null,
|
||||||
val notes: String? = null
|
val notes: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.android.trisolarisserver.controller.dto
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class PaymentCreateRequest(
|
||||||
|
val amount: Long,
|
||||||
|
val method: String,
|
||||||
|
val currency: String? = null,
|
||||||
|
val reference: String? = null,
|
||||||
|
val notes: String? = null,
|
||||||
|
val receivedAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PaymentResponse(
|
||||||
|
val id: UUID,
|
||||||
|
val bookingId: UUID,
|
||||||
|
val amount: Long,
|
||||||
|
val currency: String,
|
||||||
|
val method: String,
|
||||||
|
val reference: String?,
|
||||||
|
val notes: String?,
|
||||||
|
val receivedAt: String,
|
||||||
|
val receivedByUserId: UUID?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingBalanceResponse(
|
||||||
|
val expectedPay: Long,
|
||||||
|
val amountCollected: Long,
|
||||||
|
val pending: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.android.trisolarisserver.controller.dto
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class RatePlanCreateRequest(
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
val roomTypeCode: String,
|
||||||
|
val baseRate: Long,
|
||||||
|
val currency: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RatePlanUpdateRequest(
|
||||||
|
val name: String,
|
||||||
|
val baseRate: Long,
|
||||||
|
val currency: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RatePlanResponse(
|
||||||
|
val id: UUID,
|
||||||
|
val propertyId: UUID,
|
||||||
|
val roomTypeId: UUID,
|
||||||
|
val roomTypeCode: String,
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
val baseRate: Long,
|
||||||
|
val currency: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RateCalendarUpsertRequest(
|
||||||
|
val rateDate: String,
|
||||||
|
val rate: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RateCalendarResponse(
|
||||||
|
val id: UUID,
|
||||||
|
val ratePlanId: UUID,
|
||||||
|
val rateDate: LocalDate,
|
||||||
|
val rate: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoomStayRateChangeRequest(
|
||||||
|
val effectiveAt: String,
|
||||||
|
val nightlyRate: Long,
|
||||||
|
val rateSource: String,
|
||||||
|
val ratePlanCode: String? = null,
|
||||||
|
val currency: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoomStayRateChangeResponse(
|
||||||
|
val oldRoomStayId: UUID,
|
||||||
|
val newRoomStayId: UUID,
|
||||||
|
val effectiveAt: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RateResolveResponse(
|
||||||
|
val roomTypeCode: String,
|
||||||
|
val rateDate: LocalDate,
|
||||||
|
val rate: Long,
|
||||||
|
val currency: String,
|
||||||
|
val ratePlanCode: String? = null
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@ data class RoomTypeUpsertRequest(
|
|||||||
val maxOccupancy: Int? = null,
|
val maxOccupancy: Int? = null,
|
||||||
val sqFeet: Int? = null,
|
val sqFeet: Int? = null,
|
||||||
val bathroomSqFeet: Int? = null,
|
val bathroomSqFeet: Int? = null,
|
||||||
|
val defaultRate: Long? = null,
|
||||||
val active: Boolean? = null,
|
val active: Boolean? = null,
|
||||||
val otaAliases: Set<String>? = null,
|
val otaAliases: Set<String>? = null,
|
||||||
val amenityIds: Set<UUID>? = null
|
val amenityIds: Set<UUID>? = null
|
||||||
@@ -23,6 +24,7 @@ data class RoomTypeResponse(
|
|||||||
val maxOccupancy: Int,
|
val maxOccupancy: Int,
|
||||||
val sqFeet: Int?,
|
val sqFeet: Int?,
|
||||||
val bathroomSqFeet: Int?,
|
val bathroomSqFeet: Int?,
|
||||||
|
val defaultRate: Long?,
|
||||||
val active: Boolean,
|
val active: Boolean,
|
||||||
val otaAliases: Set<String>,
|
val otaAliases: Set<String>,
|
||||||
val amenities: Set<AmenityResponse>
|
val amenities: Set<AmenityResponse>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import com.android.trisolarisserver.models.property.Property
|
|||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
//TODO:store signature image of guest
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "guest",
|
name = "guest",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.property.AppUser
|
||||||
|
import com.android.trisolarisserver.models.property.Property
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "payment")
|
||||||
|
class Payment(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(columnDefinition = "uuid")
|
||||||
|
val id: UUID? = null,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
|
var property: Property,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "booking_id", nullable = false)
|
||||||
|
var booking: Booking,
|
||||||
|
|
||||||
|
@Column(name = "amount", nullable = false)
|
||||||
|
var amount: Long,
|
||||||
|
|
||||||
|
@Column(name = "currency", nullable = false)
|
||||||
|
var currency: String,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "method", nullable = false)
|
||||||
|
var method: PaymentMethod,
|
||||||
|
|
||||||
|
@Column(name = "reference")
|
||||||
|
var reference: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "notes")
|
||||||
|
var notes: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "received_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
var receivedAt: OffsetDateTime = OffsetDateTime.now(),
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "received_by")
|
||||||
|
var receivedBy: AppUser? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
|
enum class PaymentMethod {
|
||||||
|
CASH,
|
||||||
|
CARD,
|
||||||
|
UPI,
|
||||||
|
BANK,
|
||||||
|
ONLINE
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.android.trisolarisserver.models.room
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.property.Property
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "rate_calendar",
|
||||||
|
uniqueConstraints = [
|
||||||
|
UniqueConstraint(columnNames = ["rate_plan_id", "rate_date"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class RateCalendar(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(columnDefinition = "uuid")
|
||||||
|
val id: UUID? = null,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
|
var property: Property,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "rate_plan_id", nullable = false)
|
||||||
|
var ratePlan: RatePlan,
|
||||||
|
|
||||||
|
@Column(name = "rate_date", nullable = false)
|
||||||
|
var rateDate: LocalDate,
|
||||||
|
|
||||||
|
@Column(name = "rate", nullable = false)
|
||||||
|
var rate: Long,
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.android.trisolarisserver.models.room
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.property.Property
|
||||||
|
import jakarta.persistence.*
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "rate_plan",
|
||||||
|
uniqueConstraints = [
|
||||||
|
UniqueConstraint(columnNames = ["property_id", "code"])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class RatePlan(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(columnDefinition = "uuid")
|
||||||
|
val id: UUID? = null,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
|
var property: Property,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "room_type_id", nullable = false)
|
||||||
|
var roomType: RoomType,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var code: String,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@Column(name = "base_rate", nullable = false)
|
||||||
|
var baseRate: Long,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var currency: String,
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
val createdAt: OffsetDateTime = OffsetDateTime.now(),
|
||||||
|
|
||||||
|
@Column(name = "updated_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
var updatedAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.android.trisolarisserver.models.room
|
||||||
|
|
||||||
|
enum class RateSource {
|
||||||
|
PRESET,
|
||||||
|
NEGOTIATED,
|
||||||
|
OTA
|
||||||
|
}
|
||||||
@@ -33,6 +33,19 @@ class RoomStay(
|
|||||||
@Column(name = "to_at", columnDefinition = "timestamptz")
|
@Column(name = "to_at", columnDefinition = "timestamptz")
|
||||||
var toAt: OffsetDateTime? = null, // null = active
|
var toAt: OffsetDateTime? = null, // null = active
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "rate_source")
|
||||||
|
var rateSource: RateSource? = null,
|
||||||
|
|
||||||
|
@Column(name = "nightly_rate")
|
||||||
|
var nightlyRate: Long? = null,
|
||||||
|
|
||||||
|
@Column(name = "rate_plan_code")
|
||||||
|
var ratePlanCode: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "currency")
|
||||||
|
var currency: String? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "created_by")
|
@JoinColumn(name = "created_by")
|
||||||
var createdBy: AppUser? = null,
|
var createdBy: AppUser? = null,
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class RoomType(
|
|||||||
@Column(name = "bathroom_sq_feet")
|
@Column(name = "bathroom_sq_feet")
|
||||||
var bathroomSqFeet: Int? = null,
|
var bathroomSqFeet: Int? = null,
|
||||||
|
|
||||||
|
@Column(name = "default_rate")
|
||||||
|
var defaultRate: Long? = null,
|
||||||
|
|
||||||
@Column(name = "is_active", nullable = false)
|
@Column(name = "is_active", nullable = false)
|
||||||
var active: Boolean = true,
|
var active: Boolean = true,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.android.trisolarisserver.repo
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.booking.Payment
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.data.repository.query.Param
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface PaymentRepo : JpaRepository<Payment, UUID> {
|
||||||
|
fun findByBookingIdOrderByReceivedAtDesc(bookingId: UUID): List<Payment>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
select coalesce(sum(p.amount), 0)
|
||||||
|
from Payment p
|
||||||
|
where p.booking.id = :bookingId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun sumAmountByBookingId(@Param("bookingId") bookingId: UUID): Long
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.android.trisolarisserver.repo
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.room.RateCalendar
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface RateCalendarRepo : JpaRepository<RateCalendar, UUID> {
|
||||||
|
fun findByRatePlanIdAndRateDateBetweenOrderByRateDateAsc(
|
||||||
|
ratePlanId: UUID,
|
||||||
|
from: LocalDate,
|
||||||
|
to: LocalDate
|
||||||
|
): List<RateCalendar>
|
||||||
|
|
||||||
|
fun findByRatePlanIdAndRateDate(ratePlanId: UUID, rateDate: LocalDate): RateCalendar?
|
||||||
|
fun findByRatePlanIdAndRateDateIn(ratePlanId: UUID, dates: List<LocalDate>): List<RateCalendar>
|
||||||
|
fun findByRatePlanId(ratePlanId: UUID): List<RateCalendar>
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.android.trisolarisserver.repo
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.room.RatePlan
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface RatePlanRepo : JpaRepository<RatePlan, UUID> {
|
||||||
|
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RatePlan>
|
||||||
|
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RatePlan?
|
||||||
|
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
|
||||||
|
}
|
||||||
@@ -36,6 +36,13 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
|||||||
""")
|
""")
|
||||||
fun findActiveByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
fun findActiveByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select rs
|
||||||
|
from RoomStay rs
|
||||||
|
where rs.booking.id = :bookingId
|
||||||
|
""")
|
||||||
|
fun findByBookingId(@Param("bookingId") bookingId: UUID): List<RoomStay>
|
||||||
|
|
||||||
@Query("""
|
@Query("""
|
||||||
select rs.room.id
|
select rs.room.id
|
||||||
from RoomStay rs
|
from RoomStay rs
|
||||||
|
|||||||
Reference in New Issue
Block a user