From 3e984fdcb3f4b6766ed64ba40ff057cc35df06b1 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Thu, 29 Jan 2026 05:09:12 +0530 Subject: [PATCH] Add guest signature upload/download --- AGENTS.md | 2 + .../component/GuestSignatureStorage.kt | 44 +++++++++++++ .../trisolarisserver/controller/Guests.kt | 63 +++++++++++++++++-- .../controller/dto/OrgPropertyDtos.kt | 1 + .../trisolarisserver/models/booking/Guest.kt | 6 ++ 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/component/GuestSignatureStorage.kt diff --git a/AGENTS.md b/AGENTS.md index 4a44bef..18bce29 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/src/main/kotlin/com/android/trisolarisserver/component/GuestSignatureStorage.kt b/src/main/kotlin/com/android/trisolarisserver/component/GuestSignatureStorage.kt new file mode 100644 index 0000000..df7e8a1 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/component/GuestSignatureStorage.kt @@ -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) + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt index a283bee..e707919 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt @@ -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 { + 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.toResponse( + propertyId: UUID, guestVehicleRepo: GuestVehicleRepo, guestRatingRepo: GuestRatingRepo ): List { @@ -169,6 +219,9 @@ private fun Set.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] ) diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt b/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt index 953286f..9302e0b 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt @@ -45,6 +45,7 @@ data class GuestResponse( val phoneE164: String?, val nationality: String?, val addressText: String?, + val signatureUrl: String?, val vehicleNumbers: Set, val averageScore: Double? ) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/Guest.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/Guest.kt index eef3dfe..5c95710 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/booking/Guest.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/Guest.kt @@ -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(),