Add guest signature upload/download
All checks were successful
build-and-deploy / build-deploy (push) Successful in 34s

This commit is contained in:
androidlover5842
2026-01-29 05:09:12 +05:30
parent 71c70c8554
commit 3e984fdcb3
5 changed files with 111 additions and 5 deletions

View File

@@ -119,6 +119,8 @@ Guest APIs
- POST /properties/{propertyId}/guests
- /properties/{propertyId}/guests/search?phone=... or ?vehicleNumber=...
- /properties/{propertyId}/guests/{guestId}/vehicles (add vehicle)
- POST /properties/{propertyId}/guests/{guestId}/signature
- GET /properties/{propertyId}/guests/{guestId}/signature/file
Room stays
- POST /properties/{propertyId}/room-stays/{roomStayId}/change-rate

View File

@@ -0,0 +1,44 @@
package com.android.trisolarisserver.component
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import org.springframework.web.multipart.MultipartFile
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.UUID
data class StoredSignature(
val storagePath: String,
val sizeBytes: Long
)
@Component
class GuestSignatureStorage(
@Value("\${storage.documents.root}") private val root: String
) {
init {
val rootPath = Paths.get(root)
Files.createDirectories(rootPath)
if (!Files.isWritable(rootPath)) {
throw IllegalStateException("Guest signature root not writable: $root")
}
}
fun store(propertyId: UUID, guestId: UUID, file: MultipartFile): StoredSignature {
val dir = Paths.get(root, "guests", propertyId.toString(), guestId.toString())
Files.createDirectories(dir)
val path = dir.resolve("signature.svg")
file.inputStream.use { input ->
Files.newOutputStream(path).use { output -> input.copyTo(output) }
}
return StoredSignature(
storagePath = path.toString(),
sizeBytes = Files.size(path)
)
}
fun resolvePath(storagePath: String): Path {
return Paths.get(storagePath)
}
}

View File

@@ -1,6 +1,7 @@
package com.android.trisolarisserver.controller
import com.android.trisolarisserver.component.PropertyAccess
import com.android.trisolarisserver.component.GuestSignatureStorage
import com.android.trisolarisserver.controller.dto.GuestCreateRequest
import com.android.trisolarisserver.controller.dto.GuestResponse
import com.android.trisolarisserver.controller.dto.GuestVehicleRequest
@@ -12,9 +13,13 @@ import com.android.trisolarisserver.db.repo.GuestRatingRepo
import com.android.trisolarisserver.repo.GuestVehicleRepo
import com.android.trisolarisserver.repo.PropertyRepo
import com.android.trisolarisserver.security.MyPrincipal
import org.springframework.core.io.FileSystemResource
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import java.time.OffsetDateTime
import java.util.UUID
@@ -27,7 +32,8 @@ class Guests(
private val guestRepo: GuestRepo,
private val bookingRepo: BookingRepo,
private val guestVehicleRepo: GuestVehicleRepo,
private val guestRatingRepo: GuestRatingRepo
private val guestRatingRepo: GuestRatingRepo,
private val signatureStorage: GuestSignatureStorage
) {
@PostMapping
@@ -58,7 +64,7 @@ class Guests(
booking.primaryGuest = existing
booking.updatedAt = OffsetDateTime.now()
bookingRepo.save(booking)
return setOf(existing).toResponse(guestVehicleRepo, guestRatingRepo).first()
return setOf(existing).toResponse(propertyId, guestVehicleRepo, guestRatingRepo).first()
}
val now = OffsetDateTime.now()
@@ -74,7 +80,7 @@ class Guests(
booking.primaryGuest = saved
booking.updatedAt = now
bookingRepo.save(booking)
return setOf(saved).toResponse(guestVehicleRepo, guestRatingRepo).first()
return setOf(saved).toResponse(propertyId, guestVehicleRepo, guestRatingRepo).first()
}
@GetMapping("/search")
@@ -101,7 +107,7 @@ class Guests(
val vehicle = guestVehicleRepo.findByPropertyIdAndVehicleNumberIgnoreCase(propertyId, vehicleNumber)
if (vehicle != null) guests.add(vehicle.guest)
}
return guests.toResponse(guestVehicleRepo, guestRatingRepo)
return guests.toResponse(propertyId, guestVehicleRepo, guestRatingRepo)
}
@PostMapping("/{guestId}/vehicles")
@@ -141,12 +147,56 @@ class Guests(
)
guestVehicleRepo.save(vehicle)
}
return setOf(guest).toResponse(guestVehicleRepo, guestRatingRepo).first()
return setOf(guest).toResponse(propertyId, guestVehicleRepo, guestRatingRepo).first()
}
@PostMapping("/{guestId}/signature")
@ResponseStatus(HttpStatus.CREATED)
fun uploadSignature(
@PathVariable propertyId: UUID,
@PathVariable guestId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?,
@RequestPart("file") file: MultipartFile
): GuestResponse {
requireRole(propertyAccess, propertyId, principal, com.android.trisolarisserver.models.property.Role.ADMIN, com.android.trisolarisserver.models.property.Role.MANAGER)
if (file.isEmpty) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "File is empty")
}
val contentType = file.contentType ?: ""
if (!contentType.equals("image/svg+xml", ignoreCase = true)) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Only SVG allowed")
}
val (property, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
val stored = signatureStorage.store(propertyId, guestId, file)
guest.signaturePath = stored.storagePath
guest.signatureUpdatedAt = OffsetDateTime.now()
guestRepo.save(guest)
return setOf(guest).toResponse(propertyId, guestVehicleRepo, guestRatingRepo).first()
}
@GetMapping("/{guestId}/signature/file")
fun downloadSignature(
@PathVariable propertyId: UUID,
@PathVariable guestId: UUID,
@AuthenticationPrincipal principal: MyPrincipal?
): ResponseEntity<FileSystemResource> {
requireMember(propertyAccess, propertyId, principal)
val (_, guest) = requirePropertyGuest(propertyRepo, guestRepo, propertyId, guestId)
val path = guest.signaturePath ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Signature not found")
val resource = FileSystemResource(signatureStorage.resolvePath(path))
if (!resource.exists()) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Signature not found")
}
return ResponseEntity.ok()
.contentType(MediaType.valueOf("image/svg+xml"))
.body(resource)
}
}
private fun Set<Guest>.toResponse(
propertyId: UUID,
guestVehicleRepo: GuestVehicleRepo,
guestRatingRepo: GuestRatingRepo
): List<GuestResponse> {
@@ -169,6 +219,9 @@ private fun Set<Guest>.toResponse(
phoneE164 = guest.phoneE164,
nationality = guest.nationality,
addressText = guest.addressText,
signatureUrl = guest.signaturePath?.let {
"/properties/$propertyId/guests/${guest.id}/signature/file"
},
vehicleNumbers = vehiclesByGuest[guest.id]?.map { it.vehicleNumber }?.toSet() ?: emptySet(),
averageScore = averages[guest.id]
)

View File

@@ -45,6 +45,7 @@ data class GuestResponse(
val phoneE164: String?,
val nationality: String?,
val addressText: String?,
val signatureUrl: String?,
val vehicleNumbers: Set<String>,
val averageScore: Double?
)

View File

@@ -29,6 +29,12 @@ class Guest(
@Column(name = "address_text")
var addressText: String? = null,
@Column(name = "signature_path")
var signaturePath: String? = null,
@Column(name = "signature_updated_at", columnDefinition = "timestamptz")
var signatureUpdatedAt: OffsetDateTime? = null,
@Column(name = "created_at", columnDefinition = "timestamptz")
val createdAt: OffsetDateTime = OffsetDateTime.now(),