262 lines
11 KiB
Kotlin
262 lines
11 KiB
Kotlin
package com.android.trisolarisserver.controller
|
|
|
|
import com.android.trisolarisserver.component.PropertyAccess
|
|
import com.android.trisolarisserver.controller.dto.CardPrepareRequest
|
|
import com.android.trisolarisserver.controller.dto.CardPrepareResponse
|
|
import com.android.trisolarisserver.controller.dto.CardRevokeResponse
|
|
import com.android.trisolarisserver.controller.dto.IssueCardRequest
|
|
import com.android.trisolarisserver.controller.dto.IssuedCardResponse
|
|
import com.android.trisolarisserver.models.property.Role
|
|
import com.android.trisolarisserver.models.room.IssuedCard
|
|
import com.android.trisolarisserver.repo.AppUserRepo
|
|
import com.android.trisolarisserver.repo.IssuedCardRepo
|
|
import com.android.trisolarisserver.repo.PropertyCardCounterRepo
|
|
import com.android.trisolarisserver.repo.PropertyRepo
|
|
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.transaction.annotation.Transactional
|
|
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.OffsetDateTime
|
|
import java.util.UUID
|
|
|
|
@RestController
|
|
@RequestMapping("/properties/{propertyId}/room-stays")
|
|
class IssuedCards(
|
|
private val propertyAccess: PropertyAccess,
|
|
private val roomStayRepo: RoomStayRepo,
|
|
private val issuedCardRepo: IssuedCardRepo,
|
|
private val appUserRepo: AppUserRepo,
|
|
private val counterRepo: PropertyCardCounterRepo,
|
|
private val propertyRepo: PropertyRepo
|
|
) {
|
|
|
|
@PostMapping("/{roomStayId}/cards/prepare")
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
@Transactional
|
|
fun prepare(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable roomStayId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: CardPrepareRequest
|
|
): CardPrepareResponse {
|
|
val actor = requireIssueActor(propertyId, principal)
|
|
val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed")
|
|
|
|
val issuedAt = nowForProperty(stay.property.timezone)
|
|
val expiresAt = request.expiresAt?.let { parseOffset(it) } ?: stay.toAt
|
|
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt required")
|
|
if (!expiresAt.isAfter(issuedAt)) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt must be after issuedAt")
|
|
}
|
|
|
|
val cardIndex = nextCardIndex(stay.property.id!!)
|
|
val payload = buildSector0Payload(stay.room.roomNumber, cardIndex, issuedAt, expiresAt)
|
|
return CardPrepareResponse(
|
|
cardIndex = cardIndex,
|
|
key = payload.key,
|
|
timeData = payload.timeData,
|
|
issuedAt = issuedAt.toString(),
|
|
expiresAt = expiresAt.toString(),
|
|
sector3Block0 = encodeBlock(actor.name),
|
|
sector3Block1 = encodeBlock(actor.id?.toString()),
|
|
sector3Block2 = encodeBlock(null)
|
|
)
|
|
}
|
|
|
|
@PostMapping("/{roomStayId}/cards")
|
|
@ResponseStatus(HttpStatus.CREATED)
|
|
@Transactional
|
|
fun issue(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable roomStayId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
|
@RequestBody request: IssueCardRequest
|
|
): IssuedCardResponse {
|
|
val actor = requireIssueActor(propertyId, principal)
|
|
if (request.cardId.isBlank()) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardId required")
|
|
}
|
|
if (request.cardIndex <= 0) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "cardIndex required")
|
|
}
|
|
val stay = requireOpenRoomStayForProperty(roomStayRepo, propertyId, roomStayId, "Room stay closed")
|
|
val issuedAt = parseOffset(request.issuedAt) ?: nowForProperty(stay.property.timezone)
|
|
val expiresAt = parseOffset(request.expiresAt)
|
|
?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt required")
|
|
if (!expiresAt.isAfter(issuedAt)) {
|
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "expiresAt must be after issuedAt")
|
|
}
|
|
val now = OffsetDateTime.now()
|
|
if (issuedCardRepo.existsActiveForRoomStay(roomStayId, now)) {
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Active card already exists for room stay")
|
|
}
|
|
if (issuedCardRepo.existsActiveForRoom(propertyId, stay.room.id!!, now)) {
|
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Active card already exists for room")
|
|
}
|
|
|
|
val card = IssuedCard(
|
|
property = stay.property,
|
|
room = stay.room,
|
|
roomStay = stay,
|
|
cardId = request.cardId.trim(),
|
|
cardIndex = request.cardIndex,
|
|
issuedAt = issuedAt,
|
|
expiresAt = expiresAt,
|
|
issuedBy = actor
|
|
)
|
|
return issuedCardRepo.save(card).toResponse()
|
|
}
|
|
|
|
@GetMapping("/{roomStayId}/cards")
|
|
fun list(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable roomStayId: UUID,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): List<IssuedCardResponse> {
|
|
requireViewActor(propertyId, principal)
|
|
val stay = requireRoomStayForProperty(roomStayRepo, propertyId, roomStayId)
|
|
return issuedCardRepo.findByRoomStayIdOrderByIssuedAtDesc(roomStayId)
|
|
.map { it.toResponse() }
|
|
}
|
|
|
|
@PostMapping("/cards/{cardIndex}/revoke")
|
|
@org.springframework.transaction.annotation.Transactional
|
|
fun revoke(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable cardIndex: Int,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): CardRevokeResponse {
|
|
val card = issuedCardRepo.findByPropertyIdAndCardIndex(propertyId, cardIndex)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Card not found")
|
|
requireRevokeActor(propertyId, principal, card.roomStay == null)
|
|
if (card.revokedAt == null) {
|
|
val now = nowForProperty(card.property.timezone)
|
|
card.revokedAt = now
|
|
card.expiresAt = now
|
|
issuedCardRepo.save(card)
|
|
}
|
|
val key = buildSector0Block2(card.room.roomNumber, card.cardIndex)
|
|
val timeData = buildSector0TimeData(card.issuedAt, card.expiresAt, key)
|
|
return CardRevokeResponse(timeData = timeData)
|
|
}
|
|
|
|
@GetMapping("/cards/{cardIndex}")
|
|
fun getCardByIndex(
|
|
@PathVariable propertyId: UUID,
|
|
@PathVariable cardIndex: Int,
|
|
@AuthenticationPrincipal principal: MyPrincipal?
|
|
): IssuedCardResponse {
|
|
requireCardAdminActor(propertyId, principal)
|
|
val card = issuedCardRepo.findByPropertyIdAndCardIndex(propertyId, cardIndex)
|
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Card not found")
|
|
return card.toResponse()
|
|
}
|
|
|
|
private fun requireMember(propertyId: UUID, principal: MyPrincipal?) {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
propertyAccess.requireMember(propertyId, principal.userId)
|
|
}
|
|
|
|
private fun requireViewActor(propertyId: UUID, principal: MyPrincipal?) {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
propertyAccess.requireAnyRole(
|
|
propertyId,
|
|
principal.userId,
|
|
Role.STAFF,
|
|
Role.ADMIN,
|
|
Role.MANAGER,
|
|
Role.SUPERVISOR
|
|
)
|
|
}
|
|
|
|
private fun requireIssueActor(propertyId: UUID, principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
propertyAccess.requireMember(propertyId, principal.userId)
|
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
return appUserRepo.findById(principal.userId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
|
}
|
|
}
|
|
|
|
private fun requireRevokeActor(propertyId: UUID, principal: MyPrincipal?, isTempCard: Boolean) {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
propertyAccess.requireMember(propertyId, principal.userId)
|
|
if (isTempCard) {
|
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
} else {
|
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN)
|
|
}
|
|
}
|
|
|
|
private fun requireCardAdminActor(propertyId: UUID, principal: MyPrincipal?) {
|
|
if (principal == null) {
|
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
|
}
|
|
propertyAccess.requireMember(propertyId, principal.userId)
|
|
propertyAccess.requireAnyRole(propertyId, principal.userId, Role.ADMIN, Role.MANAGER)
|
|
}
|
|
|
|
|
|
private fun nextCardIndex(propertyId: UUID): Int {
|
|
var counter = counterRepo.findByPropertyIdForUpdate(propertyId)
|
|
if (counter == null) {
|
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
|
}
|
|
counterRepo.save(com.android.trisolarisserver.models.room.PropertyCardCounter(
|
|
property = property,
|
|
nextIndex = 10001
|
|
))
|
|
counter = counterRepo.findByPropertyIdForUpdate(propertyId)
|
|
}
|
|
val current = counter!!.nextIndex
|
|
counter.nextIndex = current + 1
|
|
counter.updatedAt = OffsetDateTime.now()
|
|
counterRepo.save(counter)
|
|
return current
|
|
}
|
|
|
|
private fun buildSector0Payload(
|
|
roomNumber: Int,
|
|
cardIndex: Int,
|
|
issuedAt: OffsetDateTime,
|
|
expiresAt: OffsetDateTime
|
|
): Sector0Payload {
|
|
val key = buildSector0Block2(roomNumber, cardIndex)
|
|
val finalData = buildSector0TimeData(issuedAt, expiresAt, key)
|
|
return Sector0Payload(key, finalData)
|
|
}
|
|
|
|
private fun buildSector0TimeData(
|
|
issuedAt: OffsetDateTime,
|
|
expiresAt: OffsetDateTime,
|
|
key: String? = null
|
|
): String {
|
|
val newData = "0000000000" + formatDateComponents(issuedAt) + formatDateComponents(expiresAt)
|
|
val checkSum = calculateChecksum((key ?: "") + newData)
|
|
return newData + checkSum
|
|
}
|
|
}
|
|
|
|
private data class Sector0Payload(
|
|
val key: String,
|
|
val timeData: String
|
|
)
|