From 72d9f5bb12f462482c4c04a89fcb31eb188f599c Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Sat, 24 Jan 2026 22:39:45 +0530 Subject: [PATCH] modes of transport --- .../trisolarisserver/controller/Guests.kt | 113 ++++++++++++++++++ .../trisolarisserver/controller/Orgs.kt | 18 ++- .../trisolarisserver/controller/Properties.kt | 18 ++- .../controller/TransportModes.kt | 52 ++++++++ .../controller/dto/OrgPropertyDtos.kt | 34 +++++- .../models/booking/Booking.kt | 7 ++ .../models/booking/GuestVehicle.kt | 32 +++++ .../models/booking/TransportMode.kt | 12 ++ .../models/property/Organization.kt | 12 +- .../models/property/Property.kt | 10 ++ .../trisolarisserver/repo/GuestVehicleRepo.kt | 11 ++ 11 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/controller/TransportModes.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/models/booking/GuestVehicle.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/models/booking/TransportMode.kt create mode 100644 src/main/kotlin/com/android/trisolarisserver/repo/GuestVehicleRepo.kt diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt new file mode 100644 index 0000000..1f5937a --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Guests.kt @@ -0,0 +1,113 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.GuestResponse +import com.android.trisolarisserver.controller.dto.GuestVehicleRequest +import com.android.trisolarisserver.models.booking.Guest +import com.android.trisolarisserver.models.booking.GuestVehicle +import com.android.trisolarisserver.repo.GuestRepo +import com.android.trisolarisserver.repo.GuestVehicleRepo +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.* +import org.springframework.web.server.ResponseStatusException +import java.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/guests") +class Guests( + private val propertyAccess: PropertyAccess, + private val propertyRepo: PropertyRepo, + private val guestRepo: GuestRepo, + private val guestVehicleRepo: GuestVehicleRepo +) { + + @GetMapping("/search") + fun search( + @PathVariable propertyId: UUID, + @AuthenticationPrincipal principal: MyPrincipal?, + @RequestParam(required = false) phone: String?, + @RequestParam(required = false) vehicleNumber: String? + ): List { + requirePrincipal(principal) + propertyAccess.requireMember(propertyId, principal!!.userId) + + if (phone.isNullOrBlank() && vehicleNumber.isNullOrBlank()) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "phone or vehicleNumber required") + } + + val property = propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") + } + val orgId = property.org.id ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Org missing") + + val guests = mutableSetOf() + if (!phone.isNullOrBlank()) { + val guest = guestRepo.findByOrgIdAndPhoneE164(orgId, phone) + if (guest != null) guests.add(guest) + } + if (!vehicleNumber.isNullOrBlank()) { + val vehicle = guestVehicleRepo.findByOrgIdAndVehicleNumberIgnoreCase(orgId, vehicleNumber) + if (vehicle != null) guests.add(vehicle.guest) + } + return guests.toResponse(guestVehicleRepo) + } + + @PostMapping("/{guestId}/vehicles") + @ResponseStatus(HttpStatus.CREATED) + fun addVehicle( + @PathVariable propertyId: UUID, + @PathVariable guestId: UUID, + @AuthenticationPrincipal principal: MyPrincipal?, + @RequestBody request: GuestVehicleRequest + ): GuestResponse { + requirePrincipal(principal) + propertyAccess.requireMember(propertyId, principal!!.userId) + + val property = propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found") + } + val guest = guestRepo.findById(guestId).orElseThrow { + ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found") + } + if (guest.org.id != property.org.id) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property org") + } + if (guestVehicleRepo.existsByOrgIdAndVehicleNumberIgnoreCase(property.org.id!!, request.vehicleNumber)) { + throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists") + } + + val vehicle = GuestVehicle( + org = property.org, + guest = guest, + vehicleNumber = request.vehicleNumber.trim() + ) + guestVehicleRepo.save(vehicle) + return listOf(guest).toResponse(guestVehicleRepo).first() + } + + private fun requirePrincipal(principal: MyPrincipal?) { + if (principal == null) { + throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal") + } + } +} + +private fun Set.toResponse(guestVehicleRepo: GuestVehicleRepo): List { + val ids = this.mapNotNull { it.id } + val vehicles = if (ids.isEmpty()) emptyList() else guestVehicleRepo.findByGuestIdIn(ids) + val vehiclesByGuest = vehicles.groupBy { it.guest.id } + return this.map { guest -> + GuestResponse( + id = guest.id!!, + orgId = guest.org.id!!, + name = guest.name, + phoneE164 = guest.phoneE164, + nationality = guest.nationality, + addressText = guest.addressText, + vehicleNumbers = vehiclesByGuest[guest.id]?.map { it.vehicleNumber }?.toSet() ?: emptySet() + ) + } +} diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt index 099becd..65da76c 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Orgs.kt @@ -5,6 +5,7 @@ import com.android.trisolarisserver.controller.dto.OrgResponse import com.android.trisolarisserver.repo.AppUserRepo import com.android.trisolarisserver.repo.OrganizationRepo import com.android.trisolarisserver.repo.PropertyUserRepo +import com.android.trisolarisserver.models.booking.TransportMode import com.android.trisolarisserver.models.property.Organization import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.security.MyPrincipal @@ -42,12 +43,16 @@ class Orgs( val org = Organization().apply { name = request.name emailAliases = request.emailAliases?.toMutableSet() ?: mutableSetOf() + if (request.allowedTransportModes != null) { + allowedTransportModes = parseTransportModes(request.allowedTransportModes) + } } val saved = orgRepo.save(org) return OrgResponse( id = saved.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"), name = saved.name ?: "", - emailAliases = saved.emailAliases.toSet() + emailAliases = saved.emailAliases.toSet(), + allowedTransportModes = saved.allowedTransportModes.map { it.name }.toSet() ) } @@ -66,7 +71,8 @@ class Orgs( return OrgResponse( id = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"), name = org.name ?: "", - emailAliases = org.emailAliases.toSet() + emailAliases = org.emailAliases.toSet(), + allowedTransportModes = org.allowedTransportModes.map { it.name }.toSet() ) } @@ -78,4 +84,12 @@ class Orgs( ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found") } } + + private fun parseTransportModes(modes: Set): MutableSet { + return try { + modes.map { TransportMode.valueOf(it) }.toMutableSet() + } catch (_: IllegalArgumentException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown transport mode") + } + } } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt index 6ad1aa3..b8cd276 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/Properties.kt @@ -16,6 +16,7 @@ import com.android.trisolarisserver.models.property.PropertyUser import com.android.trisolarisserver.models.property.PropertyUserId import com.android.trisolarisserver.models.property.Role import com.android.trisolarisserver.security.MyPrincipal +import com.android.trisolarisserver.models.booking.TransportMode import org.springframework.http.HttpStatus import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.DeleteMapping @@ -67,7 +68,8 @@ class Properties( currency = request.currency ?: "INR", active = request.active ?: true, otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf(), - emailAddresses = request.emailAddresses?.toMutableSet() ?: mutableSetOf() + emailAddresses = request.emailAddresses?.toMutableSet() ?: mutableSetOf(), + allowedTransportModes = request.allowedTransportModes?.let { parseTransportModes(it) } ?: mutableSetOf() ) val saved = propertyRepo.save(property) return saved.toResponse() @@ -215,6 +217,9 @@ class Properties( if (request.emailAddresses != null) { property.emailAddresses = request.emailAddresses.toMutableSet() } + if (request.allowedTransportModes != null) { + property.allowedTransportModes = parseTransportModes(request.allowedTransportModes) + } return propertyRepo.save(property).toResponse() } @@ -239,6 +244,14 @@ class Properties( throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role") } } + + private fun parseTransportModes(modes: Set): MutableSet { + return try { + modes.map { TransportMode.valueOf(it) }.toMutableSet() + } catch (_: IllegalArgumentException) { + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown transport mode") + } + } } private fun Property.toResponse(): PropertyResponse { @@ -254,7 +267,8 @@ private fun Property.toResponse(): PropertyResponse { currency = currency, active = active, otaAliases = otaAliases.toSet(), - emailAddresses = emailAddresses.toSet() + emailAddresses = emailAddresses.toSet(), + allowedTransportModes = allowedTransportModes.map { it.name }.toSet() ) } diff --git a/src/main/kotlin/com/android/trisolarisserver/controller/TransportModes.kt b/src/main/kotlin/com/android/trisolarisserver/controller/TransportModes.kt new file mode 100644 index 0000000..59cca37 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/controller/TransportModes.kt @@ -0,0 +1,52 @@ +package com.android.trisolarisserver.controller + +import com.android.trisolarisserver.component.PropertyAccess +import com.android.trisolarisserver.controller.dto.TransportModeStatusResponse +import com.android.trisolarisserver.models.booking.TransportMode +import com.android.trisolarisserver.repo.PropertyRepo +import com.android.trisolarisserver.security.MyPrincipal +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.util.UUID + +@RestController +@RequestMapping("/properties/{propertyId}/transport-modes") +class TransportModes( + private val propertyAccess: PropertyAccess, + private val propertyRepo: PropertyRepo +) { + + @GetMapping + fun list( + @PathVariable propertyId: UUID, + @AuthenticationPrincipal principal: MyPrincipal? + ): List { + requirePrincipal(principal) + propertyAccess.requireMember(propertyId, principal!!.userId) + + val property = propertyRepo.findById(propertyId).orElseThrow { + ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "Property not found") + } + val allowed = when { + property.allowedTransportModes.isNotEmpty() -> property.allowedTransportModes + property.org.allowedTransportModes.isNotEmpty() -> property.org.allowedTransportModes + else -> TransportMode.entries.toSet() + } + return TransportMode.entries.map { mode -> + TransportModeStatusResponse( + mode = mode.name, + enabled = allowed.contains(mode) + ) + } + } + + private fun requirePrincipal(principal: MyPrincipal?) { + if (principal == null) { + throw ResponseStatusException(org.springframework.http.HttpStatus.UNAUTHORIZED, "Missing principal") + } + } +} 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 b1f2299..a673d04 100644 --- a/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt +++ b/src/main/kotlin/com/android/trisolarisserver/controller/dto/OrgPropertyDtos.kt @@ -4,13 +4,15 @@ import java.util.UUID data class OrgCreateRequest( val name: String, - val emailAliases: Set? = null + val emailAliases: Set? = null, + val allowedTransportModes: Set? = null ) data class OrgResponse( val id: UUID, val name: String, - val emailAliases: Set + val emailAliases: Set, + val allowedTransportModes: Set ) data class PropertyCreateRequest( @@ -21,7 +23,8 @@ data class PropertyCreateRequest( val currency: String? = null, val active: Boolean? = null, val otaAliases: Set? = null, - val emailAddresses: Set? = null + val emailAddresses: Set? = null, + val allowedTransportModes: Set? = null ) data class PropertyUpdateRequest( @@ -32,7 +35,8 @@ data class PropertyUpdateRequest( val currency: String? = null, val active: Boolean? = null, val otaAliases: Set? = null, - val emailAddresses: Set? = null + val emailAddresses: Set? = null, + val allowedTransportModes: Set? = null ) data class PropertyResponse( @@ -45,7 +49,27 @@ data class PropertyResponse( val currency: String, val active: Boolean, val otaAliases: Set, - val emailAddresses: Set + val emailAddresses: Set, + val allowedTransportModes: Set +) + +data class GuestResponse( + val id: UUID, + val orgId: UUID, + val name: String?, + val phoneE164: String?, + val nationality: String?, + val addressText: String?, + val vehicleNumbers: Set +) + +data class GuestVehicleRequest( + val vehicleNumber: String +) + +data class TransportModeStatusResponse( + val mode: String, + val enabled: Boolean ) data class UserResponse( diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/Booking.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/Booking.kt index 464dec4..ba302f9 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/booking/Booking.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/Booking.kt @@ -52,6 +52,13 @@ class Booking( @Column(name = "email_audit_pdf_url") var emailAuditPdfUrl: String? = null, + @Enumerated(EnumType.STRING) + @Column(name = "transport_mode") + var transportMode: TransportMode? = null, + + @Column(name = "transport_vehicle_number") + var transportVehicleNumber: String? = null, + var notes: String? = null, @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestVehicle.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestVehicle.kt new file mode 100644 index 0000000..d3f9c4b --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/GuestVehicle.kt @@ -0,0 +1,32 @@ +package com.android.trisolarisserver.models.booking + +import com.android.trisolarisserver.models.property.Organization +import jakarta.persistence.* +import java.time.OffsetDateTime +import java.util.UUID + +@Entity +@Table( + name = "guest_vehicle", + uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "vehicle_number"])] +) +class GuestVehicle( + @Id + @GeneratedValue + @Column(columnDefinition = "uuid") + val id: UUID? = null, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "org_id", nullable = false) + var org: Organization, + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "guest_id", nullable = false) + var guest: Guest, + + @Column(name = "vehicle_number", nullable = false) + var vehicleNumber: String, + + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") + val createdAt: OffsetDateTime = OffsetDateTime.now() +) diff --git a/src/main/kotlin/com/android/trisolarisserver/models/booking/TransportMode.kt b/src/main/kotlin/com/android/trisolarisserver/models/booking/TransportMode.kt new file mode 100644 index 0000000..c5c102e --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/models/booking/TransportMode.kt @@ -0,0 +1,12 @@ +package com.android.trisolarisserver.models.booking + +enum class TransportMode { + CAR, + BIKE, + TRAIN, + PLANE, + BUS, + FOOT, + CYCLE, + OTHER +} diff --git a/src/main/kotlin/com/android/trisolarisserver/models/property/Organization.kt b/src/main/kotlin/com/android/trisolarisserver/models/property/Organization.kt index 80833c8..7f0f1ea 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/property/Organization.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/property/Organization.kt @@ -20,7 +20,17 @@ class Organization { joinColumns = [JoinColumn(name = "org_id")] ) @Column(name = "email", nullable = false) - var emailAliases: MutableSet = mutableSetOf() + var emailAliases: MutableSet = mutableSetOf(), + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "org_transport_mode", + joinColumns = [JoinColumn(name = "org_id")] + ) + @Column(name = "mode", nullable = false) + @Enumerated(EnumType.STRING) + var allowedTransportModes: MutableSet = + mutableSetOf(), @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") val createdAt: OffsetDateTime = OffsetDateTime.now() diff --git a/src/main/kotlin/com/android/trisolarisserver/models/property/Property.kt b/src/main/kotlin/com/android/trisolarisserver/models/property/Property.kt index 89c6e0c..fb27450 100644 --- a/src/main/kotlin/com/android/trisolarisserver/models/property/Property.kt +++ b/src/main/kotlin/com/android/trisolarisserver/models/property/Property.kt @@ -53,6 +53,16 @@ class Property( @Column(name = "alias", nullable = false) var otaAliases: MutableSet = mutableSetOf(), + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "property_transport_mode", + joinColumns = [JoinColumn(name = "property_id")] + ) + @Column(name = "mode", nullable = false) + @Enumerated(EnumType.STRING) + var allowedTransportModes: MutableSet = + mutableSetOf(), + @Column(name = "created_at", nullable = false, columnDefinition = "timestamptz") val createdAt: OffsetDateTime = OffsetDateTime.now() ) diff --git a/src/main/kotlin/com/android/trisolarisserver/repo/GuestVehicleRepo.kt b/src/main/kotlin/com/android/trisolarisserver/repo/GuestVehicleRepo.kt new file mode 100644 index 0000000..9521e78 --- /dev/null +++ b/src/main/kotlin/com/android/trisolarisserver/repo/GuestVehicleRepo.kt @@ -0,0 +1,11 @@ +package com.android.trisolarisserver.repo + +import com.android.trisolarisserver.models.booking.GuestVehicle +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface GuestVehicleRepo : JpaRepository { + fun findByOrgIdAndVehicleNumberIgnoreCase(orgId: UUID, vehicleNumber: String): GuestVehicle? + fun findByGuestIdIn(guestIds: List): List + fun existsByOrgIdAndVehicleNumberIgnoreCase(orgId: UUID, vehicleNumber: String): Boolean +}