Remove org model; make AppUser global with super admin
All checks were successful
build-and-deploy / build-deploy (push) Successful in 27s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 27s
This commit is contained in:
25
AGENTS.md
25
AGENTS.md
@@ -22,7 +22,7 @@ Core principles
|
||||
- Room availability by room number; toAt=null means occupied.
|
||||
- Room change = close old RoomStay + open new one.
|
||||
- Multi-property: every domain object scoped to property_id.
|
||||
- Users belong to org; access granted per property.
|
||||
- AppUser is global; access granted per property.
|
||||
|
||||
Immutable rules
|
||||
- Use Kotlin only; no microservices.
|
||||
@@ -46,19 +46,18 @@ Security/Auth
|
||||
- /auth/verify and /auth/me.
|
||||
|
||||
Domain entities
|
||||
- Organization: name, emailAliases, allowedTransportModes.
|
||||
- Property: code, name, addressText, emailAddresses, otaAliases, allowedTransportModes.
|
||||
- AppUser, PropertyUser (roles per property).
|
||||
- AppUser (global, superAdmin), PropertyUser (roles per property).
|
||||
- RoomType: code/name/occupancy + otaAliases.
|
||||
- Room: roomNumber, floor, hasNfc, active, maintenance, notes.
|
||||
- Booking: status, expected check-in/out, emailAuditPdfUrl, transportMode, transportVehicleNumber.
|
||||
- Guest (org-scoped).
|
||||
- Guest (property-scoped).
|
||||
- RoomStay.
|
||||
- RoomStayChange (idempotent room move).
|
||||
- IssuedCard (cardId, cardIndex, issuedAt, expiresAt, issuedBy, revokedAt).
|
||||
- PropertyCardCounter (per-property cardIndex counter).
|
||||
- GuestDocument (files + AI-extracted json).
|
||||
- GuestVehicle (org-scoped vehicle numbers).
|
||||
- GuestVehicle (property-scoped vehicle numbers).
|
||||
- InboundEmail (audit PDF + raw EML, extracted json, status).
|
||||
- RoomImage (original + thumbnail).
|
||||
|
||||
@@ -68,14 +67,10 @@ Auth
|
||||
- /auth/verify
|
||||
- /auth/me
|
||||
|
||||
Organizations / Properties / Users
|
||||
- POST /orgs
|
||||
- GET /orgs/{orgId}
|
||||
- POST /orgs/{orgId}/properties
|
||||
- GET /orgs/{orgId}/properties
|
||||
Properties / Users
|
||||
- POST /properties (creator becomes ADMIN on that property)
|
||||
- GET /properties (super admin gets all; others get memberships)
|
||||
- PUT /properties/{propertyId}
|
||||
- GET /orgs/{orgId}/users
|
||||
- POST /orgs/{orgId}/users (removed; users created by app)
|
||||
- GET /properties/{propertyId}/users
|
||||
- PUT /properties/{propertyId}/users/{userId}/roles
|
||||
- DELETE /properties/{propertyId}/users/{userId} (ADMIN only)
|
||||
@@ -93,9 +88,8 @@ Room types
|
||||
- PUT /properties/{propertyId}/room-types/{roomTypeId}
|
||||
- DELETE /properties/{propertyId}/room-types/{roomTypeId}
|
||||
|
||||
Properties / Orgs
|
||||
Properties
|
||||
- Property create/update accepts addressText, otaAliases, emailAddresses, allowedTransportModes.
|
||||
- Org create/get returns emailAliases + allowedTransportModes.
|
||||
|
||||
Booking flow
|
||||
- /properties/{propertyId}/bookings/{bookingId}/check-in (creates RoomStay rows)
|
||||
@@ -126,7 +120,7 @@ Room images
|
||||
- Thumbnails generated (320px).
|
||||
|
||||
Transport modes
|
||||
- /properties/{propertyId}/transport-modes -> returns enabled list (property > org > default all).
|
||||
- /properties/{propertyId}/transport-modes -> returns enabled list (property or default all).
|
||||
|
||||
Inbound email ingestion
|
||||
- IMAP poller (1 min) with enable flag.
|
||||
@@ -153,5 +147,6 @@ Config
|
||||
|
||||
Notes / constraints
|
||||
- Users are created by app; API only manages roles.
|
||||
- Super admin can create properties and assign users to properties.
|
||||
- Admin can assign ADMIN/MANAGER/STAFF/AGENT; Manager can assign STAFF/AGENT.
|
||||
- Agents can only see free rooms.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.android.trisolarisserver.component
|
||||
|
||||
import com.android.trisolarisserver.repo.AppUserRepo
|
||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||
import com.android.trisolarisserver.models.property.Role
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
@@ -8,14 +9,19 @@ import java.util.UUID
|
||||
|
||||
@Component
|
||||
class PropertyAccess(
|
||||
private val repo: PropertyUserRepo
|
||||
private val repo: PropertyUserRepo,
|
||||
private val appUserRepo: AppUserRepo
|
||||
) {
|
||||
fun requireMember(propertyId: UUID, userId: UUID) {
|
||||
val user = appUserRepo.findById(userId).orElse(null)
|
||||
if (user?.superAdmin == true) return
|
||||
if (!repo.existsByIdPropertyIdAndIdUserId(propertyId, userId))
|
||||
throw AccessDeniedException("No access to property")
|
||||
}
|
||||
|
||||
fun requireAnyRole(propertyId: UUID, userId: UUID, vararg roles: Role) {
|
||||
val user = appUserRepo.findById(userId).orElse(null)
|
||||
if (user?.superAdmin == true) return
|
||||
if (!repo.hasAnyRole(propertyId, userId, roles.toSet()))
|
||||
throw AccessDeniedException("Missing role")
|
||||
}
|
||||
|
||||
@@ -261,10 +261,10 @@ class BookingFlow(
|
||||
property: com.android.trisolarisserver.models.property.Property,
|
||||
mode: TransportMode
|
||||
): Boolean {
|
||||
val allowed = when {
|
||||
property.allowedTransportModes.isNotEmpty() -> property.allowedTransportModes
|
||||
property.org.allowedTransportModes.isNotEmpty() -> property.org.allowedTransportModes
|
||||
else -> TransportMode.entries.toSet()
|
||||
val allowed = if (property.allowedTransportModes.isNotEmpty()) {
|
||||
property.allowedTransportModes
|
||||
} else {
|
||||
TransportMode.entries.toSet()
|
||||
}
|
||||
return allowed.contains(mode)
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ class GuestDocuments(
|
||||
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 (guest.property.id != property.id) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||
}
|
||||
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||
|
||||
@@ -51,8 +51,8 @@ class GuestRatings(
|
||||
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 (guest.property.id != property.id) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||
}
|
||||
|
||||
val booking = bookingRepo.findById(request.bookingId).orElseThrow {
|
||||
@@ -70,7 +70,6 @@ class GuestRatings(
|
||||
|
||||
val score = parseScore(request.score)
|
||||
val rating = GuestRating(
|
||||
org = property.org,
|
||||
property = property,
|
||||
guest = guest,
|
||||
booking = booking,
|
||||
@@ -97,8 +96,8 @@ class GuestRatings(
|
||||
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 (guest.property.id != property.id) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||
}
|
||||
|
||||
return guestRatingRepo.findByGuestIdOrderByCreatedAtDesc(guestId).map { it.toResponse() }
|
||||
@@ -117,7 +116,6 @@ class GuestRatings(
|
||||
private fun GuestRating.toResponse(): GuestRatingResponse {
|
||||
return GuestRatingResponse(
|
||||
id = id!!,
|
||||
orgId = org.id!!,
|
||||
propertyId = property.id!!,
|
||||
guestId = guest.id!!,
|
||||
bookingId = booking.id!!,
|
||||
|
||||
@@ -43,15 +43,14 @@ class Guests(
|
||||
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<Guest>()
|
||||
if (!phone.isNullOrBlank()) {
|
||||
val guest = guestRepo.findByOrgIdAndPhoneE164(orgId, phone)
|
||||
val guest = guestRepo.findByPropertyIdAndPhoneE164(propertyId, phone)
|
||||
if (guest != null) guests.add(guest)
|
||||
}
|
||||
if (!vehicleNumber.isNullOrBlank()) {
|
||||
val vehicle = guestVehicleRepo.findByOrgIdAndVehicleNumberIgnoreCase(orgId, vehicleNumber)
|
||||
val vehicle = guestVehicleRepo.findByPropertyIdAndVehicleNumberIgnoreCase(propertyId, vehicleNumber)
|
||||
if (vehicle != null) guests.add(vehicle.guest)
|
||||
}
|
||||
return guests.toResponse(guestVehicleRepo, guestRatingRepo)
|
||||
@@ -74,15 +73,15 @@ class Guests(
|
||||
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 (guest.property.id != property.id) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||
}
|
||||
if (guestVehicleRepo.existsByOrgIdAndVehicleNumberIgnoreCase(property.org.id!!, request.vehicleNumber)) {
|
||||
if (guestVehicleRepo.existsByPropertyIdAndVehicleNumberIgnoreCase(property.id!!, request.vehicleNumber)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists")
|
||||
}
|
||||
|
||||
val vehicle = GuestVehicle(
|
||||
org = property.org,
|
||||
property = property,
|
||||
guest = guest,
|
||||
vehicleNumber = request.vehicleNumber.trim()
|
||||
)
|
||||
@@ -116,7 +115,6 @@ private fun Set<Guest>.toResponse(
|
||||
return this.map { guest ->
|
||||
GuestResponse(
|
||||
id = guest.id!!,
|
||||
orgId = guest.org.id!!,
|
||||
name = guest.name,
|
||||
phoneE164 = guest.phoneE164,
|
||||
nationality = guest.nationality,
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
package com.android.trisolarisserver.controller
|
||||
|
||||
import com.android.trisolarisserver.controller.dto.OrgCreateRequest
|
||||
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
|
||||
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.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/orgs")
|
||||
class Orgs(
|
||||
private val orgRepo: OrganizationRepo,
|
||||
private val appUserRepo: AppUserRepo,
|
||||
private val propertyUserRepo: PropertyUserRepo
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun createOrg(
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: OrgCreateRequest
|
||||
): OrgResponse {
|
||||
val user = requireUser(principal)
|
||||
val orgId = user.org.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Org missing")
|
||||
if (!propertyUserRepo.hasAnyRoleInOrg(orgId, user.id!!, setOf(Role.ADMIN))) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
|
||||
}
|
||||
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(),
|
||||
allowedTransportModes = saved.allowedTransportModes.map { it.name }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/{orgId}")
|
||||
fun getOrg(
|
||||
@PathVariable orgId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): OrgResponse {
|
||||
val user = requireUser(principal)
|
||||
if (user.org.id != orgId) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
||||
}
|
||||
val org = orgRepo.findById(orgId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
|
||||
}
|
||||
return OrgResponse(
|
||||
id = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing"),
|
||||
name = org.name ?: "",
|
||||
emailAliases = org.emailAliases.toSet(),
|
||||
allowedTransportModes = org.allowedTransportModes.map { it.name }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
private fun requireUser(principal: MyPrincipal?): com.android.trisolarisserver.models.property.AppUser {
|
||||
if (principal == null) {
|
||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||
}
|
||||
return appUserRepo.findById(principal.userId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTransportModes(modes: Set<String>): MutableSet<TransportMode> {
|
||||
return try {
|
||||
modes.map { TransportMode.valueOf(it) }.toMutableSet()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown transport mode")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ import com.android.trisolarisserver.controller.dto.PropertyResponse
|
||||
import com.android.trisolarisserver.controller.dto.PropertyUpdateRequest
|
||||
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
|
||||
import com.android.trisolarisserver.controller.dto.PropertyUserRoleRequest
|
||||
import com.android.trisolarisserver.controller.dto.UserResponse
|
||||
import com.android.trisolarisserver.repo.AppUserRepo
|
||||
import com.android.trisolarisserver.repo.OrganizationRepo
|
||||
import com.android.trisolarisserver.repo.PropertyRepo
|
||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||
import com.android.trisolarisserver.models.property.Property
|
||||
@@ -34,33 +32,22 @@ import java.util.UUID
|
||||
class Properties(
|
||||
private val propertyAccess: PropertyAccess,
|
||||
private val propertyRepo: PropertyRepo,
|
||||
private val orgRepo: OrganizationRepo,
|
||||
private val propertyUserRepo: PropertyUserRepo,
|
||||
private val appUserRepo: AppUserRepo
|
||||
) {
|
||||
|
||||
@PostMapping("/orgs/{orgId}/properties")
|
||||
@PostMapping("/properties")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun createProperty(
|
||||
@PathVariable orgId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||
@RequestBody request: PropertyCreateRequest
|
||||
): PropertyResponse {
|
||||
val user = requireUser(principal)
|
||||
if (user.org.id != orgId) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
||||
}
|
||||
requireOrgRole(orgId, user.id!!, Role.ADMIN)
|
||||
|
||||
if (propertyRepo.existsByOrgIdAndCode(orgId, request.code)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
|
||||
if (propertyRepo.existsByCode(request.code)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists")
|
||||
}
|
||||
|
||||
val org = orgRepo.findById(orgId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
|
||||
}
|
||||
val property = Property(
|
||||
org = org,
|
||||
code = request.code,
|
||||
name = request.name,
|
||||
addressText = request.addressText,
|
||||
@@ -72,33 +59,34 @@ class Properties(
|
||||
allowedTransportModes = request.allowedTransportModes?.let { parseTransportModes(it) } ?: mutableSetOf()
|
||||
)
|
||||
val saved = propertyRepo.save(property)
|
||||
|
||||
val creatorId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing")
|
||||
val propertyUserId = PropertyUserId(propertyId = saved.id!!, userId = creatorId)
|
||||
if (!propertyUserRepo.existsById(propertyUserId)) {
|
||||
propertyUserRepo.save(
|
||||
PropertyUser(
|
||||
id = propertyUserId,
|
||||
property = saved,
|
||||
user = user,
|
||||
roles = mutableSetOf(Role.ADMIN)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return saved.toResponse()
|
||||
}
|
||||
|
||||
@GetMapping("/orgs/{orgId}/properties")
|
||||
@GetMapping("/properties")
|
||||
fun listProperties(
|
||||
@PathVariable orgId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): List<PropertyResponse> {
|
||||
val user = requireUser(principal)
|
||||
if (user.org.id != orgId) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
||||
return if (user.superAdmin) {
|
||||
propertyRepo.findAll().map { it.toResponse() }
|
||||
} else {
|
||||
val propertyIds = propertyUserRepo.findByIdUserId(user.id!!).map { it.id.propertyId!! }
|
||||
propertyRepo.findAllById(propertyIds).map { it.toResponse() }
|
||||
}
|
||||
val propertyIds = propertyUserRepo.findPropertyIdsByOrgAndUser(orgId, user.id!!)
|
||||
return propertyRepo.findAllById(propertyIds).map { it.toResponse() }
|
||||
}
|
||||
|
||||
@GetMapping("/orgs/{orgId}/users")
|
||||
fun listUsers(
|
||||
@PathVariable orgId: UUID,
|
||||
@AuthenticationPrincipal principal: MyPrincipal?
|
||||
): List<UserResponse> {
|
||||
val user = requireUser(principal)
|
||||
if (user.org.id != orgId) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
||||
}
|
||||
requireOrgRole(orgId, user.id!!, Role.ADMIN, Role.MANAGER)
|
||||
return appUserRepo.findByOrgId(orgId).map { it.toUserResponse() }
|
||||
}
|
||||
|
||||
@GetMapping("/properties/{propertyId}/users")
|
||||
@@ -153,9 +141,6 @@ class Properties(
|
||||
val targetUser = appUserRepo.findById(userId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
|
||||
}
|
||||
if (targetUser.org.id != property.org.id) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "User not in property org")
|
||||
}
|
||||
|
||||
val propertyUser = PropertyUser(
|
||||
id = PropertyUserId(propertyId = propertyId, userId = userId),
|
||||
@@ -201,8 +186,8 @@ class Properties(
|
||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||
}
|
||||
if (propertyRepo.existsByOrgIdAndCodeAndIdNot(property.org.id!!, request.code, propertyId)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
|
||||
if (propertyRepo.existsByCodeAndIdNot(request.code, propertyId)) {
|
||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists")
|
||||
}
|
||||
|
||||
property.code = request.code
|
||||
@@ -239,12 +224,6 @@ class Properties(
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireOrgRole(orgId: UUID, userId: UUID, vararg roles: Role) {
|
||||
if (!propertyUserRepo.hasAnyRoleInOrg(orgId, userId, roles.toSet())) {
|
||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Missing role")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTransportModes(modes: Set<String>): MutableSet<TransportMode> {
|
||||
return try {
|
||||
modes.map { TransportMode.valueOf(it) }.toMutableSet()
|
||||
@@ -256,10 +235,8 @@ class Properties(
|
||||
|
||||
private fun Property.toResponse(): PropertyResponse {
|
||||
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
|
||||
val orgId = org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing")
|
||||
return PropertyResponse(
|
||||
id = id,
|
||||
orgId = orgId,
|
||||
code = code,
|
||||
name = name,
|
||||
addressText = addressText,
|
||||
@@ -271,16 +248,3 @@ private fun Property.toResponse(): PropertyResponse {
|
||||
allowedTransportModes = allowedTransportModes.map { it.name }.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
private fun com.android.trisolarisserver.models.property.AppUser.toUserResponse(): UserResponse {
|
||||
val id = this.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "User id missing")
|
||||
val orgId = this.org.id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Org id missing")
|
||||
return UserResponse(
|
||||
id = id,
|
||||
orgId = orgId,
|
||||
firebaseUid = firebaseUid,
|
||||
phoneE164 = phoneE164,
|
||||
name = name,
|
||||
disabled = disabled
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ class TransportModes(
|
||||
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()
|
||||
val allowed = if (property.allowedTransportModes.isNotEmpty()) {
|
||||
property.allowedTransportModes
|
||||
} else {
|
||||
TransportMode.entries.toSet()
|
||||
}
|
||||
return TransportMode.entries.map { mode ->
|
||||
TransportModeStatusResponse(
|
||||
|
||||
@@ -10,7 +10,6 @@ data class GuestRatingCreateRequest(
|
||||
|
||||
data class GuestRatingResponse(
|
||||
val id: UUID,
|
||||
val orgId: UUID,
|
||||
val propertyId: UUID,
|
||||
val guestId: UUID,
|
||||
val bookingId: UUID,
|
||||
|
||||
@@ -2,19 +2,6 @@ package com.android.trisolarisserver.controller.dto
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
data class OrgCreateRequest(
|
||||
val name: String,
|
||||
val emailAliases: Set<String>? = null,
|
||||
val allowedTransportModes: Set<String>? = null
|
||||
)
|
||||
|
||||
data class OrgResponse(
|
||||
val id: UUID,
|
||||
val name: String,
|
||||
val emailAliases: Set<String>,
|
||||
val allowedTransportModes: Set<String>
|
||||
)
|
||||
|
||||
data class PropertyCreateRequest(
|
||||
val code: String,
|
||||
val name: String,
|
||||
@@ -41,7 +28,6 @@ data class PropertyUpdateRequest(
|
||||
|
||||
data class PropertyResponse(
|
||||
val id: UUID,
|
||||
val orgId: UUID,
|
||||
val code: String,
|
||||
val name: String,
|
||||
val addressText: String?,
|
||||
@@ -55,7 +41,6 @@ data class PropertyResponse(
|
||||
|
||||
data class GuestResponse(
|
||||
val id: UUID,
|
||||
val orgId: UUID,
|
||||
val name: String?,
|
||||
val phoneE164: String?,
|
||||
val nationality: String?,
|
||||
@@ -75,11 +60,11 @@ data class TransportModeStatusResponse(
|
||||
|
||||
data class UserResponse(
|
||||
val id: UUID,
|
||||
val orgId: UUID,
|
||||
val firebaseUid: String?,
|
||||
val phoneE164: String?,
|
||||
val name: String?,
|
||||
val disabled: Boolean
|
||||
val disabled: Boolean,
|
||||
val superAdmin: Boolean
|
||||
)
|
||||
|
||||
data class PropertyUserRoleRequest(
|
||||
|
||||
@@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface GuestRepo : JpaRepository<Guest, UUID> {
|
||||
fun findByOrgIdAndPhoneE164(orgId: UUID, phoneE164: String): Guest?
|
||||
fun findByPropertyIdAndPhoneE164(propertyId: UUID, phoneE164: String): Guest?
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.android.trisolarisserver.models.booking
|
||||
|
||||
import com.android.trisolarisserver.models.property.Organization
|
||||
import com.android.trisolarisserver.models.property.Property
|
||||
import jakarta.persistence.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
||||
@Entity
|
||||
@Table(
|
||||
name = "guest",
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "phone_e164"])]
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["property_id", "phone_e164"])]
|
||||
)
|
||||
class Guest(
|
||||
@Id
|
||||
@@ -17,8 +17,8 @@ class Guest(
|
||||
val id: UUID? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "org_id", nullable = false)
|
||||
var org: Organization,
|
||||
@JoinColumn(name = "property_id", nullable = false)
|
||||
var property: Property,
|
||||
|
||||
@Column(name = "phone_e164")
|
||||
var phoneE164: String? = null,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.android.trisolarisserver.models.booking
|
||||
|
||||
import com.android.trisolarisserver.models.property.AppUser
|
||||
import com.android.trisolarisserver.models.property.Organization
|
||||
import com.android.trisolarisserver.models.property.Property
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
@@ -30,10 +29,6 @@ class GuestRating(
|
||||
@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 = "property_id", nullable = false)
|
||||
var property: Property,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.android.trisolarisserver.models.booking
|
||||
|
||||
import com.android.trisolarisserver.models.property.Organization
|
||||
import com.android.trisolarisserver.models.property.Property
|
||||
import jakarta.persistence.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
||||
@Entity
|
||||
@Table(
|
||||
name = "guest_vehicle",
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "vehicle_number"])]
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["property_id", "vehicle_number"])]
|
||||
)
|
||||
class GuestVehicle(
|
||||
@Id
|
||||
@@ -17,8 +17,8 @@ class GuestVehicle(
|
||||
val id: UUID? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "org_id", nullable = false)
|
||||
var org: Organization,
|
||||
@JoinColumn(name = "property_id", nullable = false)
|
||||
var property: Property,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "guest_id", nullable = false)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.android.trisolarisserver.models.property
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@@ -15,10 +20,6 @@ class AppUser(
|
||||
@Column(columnDefinition = "uuid")
|
||||
val id: UUID? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "org_id", nullable = false)
|
||||
var org: Organization,
|
||||
|
||||
@Column(name = "firebase_uid")
|
||||
var firebaseUid: String? = null, // optional if using firebase
|
||||
|
||||
@@ -27,6 +28,9 @@ class AppUser(
|
||||
|
||||
var name: String? = null,
|
||||
|
||||
@Column(name = "is_super_admin", nullable = false)
|
||||
var superAdmin: Boolean = false,
|
||||
|
||||
@Column(name = "is_disabled", nullable = false)
|
||||
var disabled: Boolean = false,
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.android.trisolarisserver.models.property
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.*
|
||||
|
||||
@Entity
|
||||
@Table(name = "organization")
|
||||
class Organization {
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(columnDefinition = "uuid")
|
||||
val id: UUID? = null
|
||||
@Column(nullable = false)
|
||||
var name: String? = null
|
||||
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(
|
||||
name = "org_email_alias",
|
||||
joinColumns = [JoinColumn(name = "org_id")]
|
||||
)
|
||||
@Column(name = "email", nullable = false)
|
||||
var emailAliases: MutableSet<String> = 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<com.android.trisolarisserver.models.booking.TransportMode> =
|
||||
mutableSetOf()
|
||||
|
||||
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.android.trisolarisserver.models.property
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "pending_user",
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["firebase_uid"])]
|
||||
)
|
||||
class PendingUser(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(columnDefinition = "uuid")
|
||||
val id: UUID? = null,
|
||||
|
||||
@Column(name = "firebase_uid", nullable = false)
|
||||
var firebaseUid: String,
|
||||
|
||||
@Column(name = "phone_e164")
|
||||
var phoneE164: String? = null,
|
||||
|
||||
var name: String? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||
)
|
||||
@@ -1,13 +1,24 @@
|
||||
package com.android.trisolarisserver.models.property
|
||||
|
||||
import jakarta.persistence.*
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.ElementCollection
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.FetchType
|
||||
import jakarta.persistence.GeneratedValue
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.JoinColumn
|
||||
import jakarta.persistence.CollectionTable
|
||||
import jakarta.persistence.Table
|
||||
import jakarta.persistence.UniqueConstraint
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "property",
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "code"])]
|
||||
uniqueConstraints = [UniqueConstraint(columnNames = ["code"])]
|
||||
)
|
||||
class Property(
|
||||
@Id
|
||||
@@ -15,10 +26,6 @@ class Property(
|
||||
@Column(columnDefinition = "uuid")
|
||||
val id: UUID? = null,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "org_id", nullable = false)
|
||||
var org: Organization,
|
||||
|
||||
@Column(nullable = false)
|
||||
var code: String, // "TRI-VNS"
|
||||
|
||||
|
||||
@@ -7,5 +7,4 @@ import java.util.UUID
|
||||
interface AppUserRepo : JpaRepository<AppUser, UUID> {
|
||||
fun findByFirebaseUid(firebaseUid: String): AppUser?
|
||||
fun existsByFirebaseUid(firebaseUid: String): Boolean
|
||||
fun findByOrgId(orgId: UUID): List<AppUser>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface GuestVehicleRepo : JpaRepository<GuestVehicle, UUID> {
|
||||
fun findByOrgIdAndVehicleNumberIgnoreCase(orgId: UUID, vehicleNumber: String): GuestVehicle?
|
||||
fun findByPropertyIdAndVehicleNumberIgnoreCase(propertyId: UUID, vehicleNumber: String): GuestVehicle?
|
||||
fun findByGuestIdIn(guestIds: List<UUID>): List<GuestVehicle>
|
||||
fun existsByOrgIdAndVehicleNumberIgnoreCase(orgId: UUID, vehicleNumber: String): Boolean
|
||||
fun existsByPropertyIdAndVehicleNumberIgnoreCase(propertyId: UUID, vehicleNumber: String): Boolean
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.android.trisolarisserver.repo
|
||||
|
||||
import com.android.trisolarisserver.models.property.Organization
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface OrganizationRepo : JpaRepository<Organization, UUID> {
|
||||
fun findByName(name: String): Organization?
|
||||
fun findByNameIgnoreCase(name: String): Organization?
|
||||
fun existsByNameIgnoreCase(name: String): Boolean
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.android.trisolarisserver.repo
|
||||
|
||||
import com.android.trisolarisserver.models.property.PendingUser
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface PendingUserRepo : JpaRepository<PendingUser, UUID> {
|
||||
fun findByFirebaseUid(firebaseUid: String): PendingUser?
|
||||
}
|
||||
@@ -5,6 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.util.UUID
|
||||
|
||||
interface PropertyRepo : JpaRepository<Property, UUID> {
|
||||
fun existsByOrgIdAndCode(orgId: UUID, code: String): Boolean
|
||||
fun existsByOrgIdAndCodeAndIdNot(orgId: UUID, code: String, id: UUID): Boolean
|
||||
fun existsByCode(code: String): Boolean
|
||||
fun existsByCodeAndIdNot(code: String, id: UUID): Boolean
|
||||
}
|
||||
|
||||
@@ -25,16 +25,6 @@ interface PropertyUserRepo : JpaRepository<PropertyUser, PropertyUserId> {
|
||||
@Param("userId") userId: UUID
|
||||
): Set<Role>
|
||||
|
||||
@Query("""
|
||||
select pu.property.id
|
||||
from PropertyUser pu
|
||||
where pu.user.id = :userId
|
||||
and pu.property.org.id = :orgId
|
||||
""")
|
||||
fun findPropertyIdsByOrgAndUser(
|
||||
@Param("orgId") orgId: UUID,
|
||||
@Param("userId") userId: UUID
|
||||
): List<UUID>
|
||||
|
||||
@Query("""
|
||||
select case when count(pu) > 0 then true else false end
|
||||
@@ -49,16 +39,4 @@ interface PropertyUserRepo : JpaRepository<PropertyUser, PropertyUserId> {
|
||||
@Param("roles") roles: Set<Role>
|
||||
): Boolean
|
||||
|
||||
@Query("""
|
||||
select case when count(pu) > 0 then true else false end
|
||||
from PropertyUser pu join pu.roles r
|
||||
where pu.user.id = :userId
|
||||
and pu.property.org.id = :orgId
|
||||
and r in :roles
|
||||
""")
|
||||
fun hasAnyRoleInOrg(
|
||||
@Param("orgId") orgId: UUID,
|
||||
@Param("userId") userId: UUID,
|
||||
@Param("roles") roles: Set<Role>
|
||||
): Boolean
|
||||
}
|
||||
|
||||
@@ -157,11 +157,11 @@ class EmailIngestionService(
|
||||
private fun resolveGuest(property: Property, extracted: Map<String, String>): Guest {
|
||||
val phone = extracted["guestPhone"]?.takeIf { !it.contains("NONE", true) }?.trim()
|
||||
if (!phone.isNullOrBlank()) {
|
||||
val existing = guestRepo.findByOrgIdAndPhoneE164(property.org.id!!, phone)
|
||||
val existing = guestRepo.findByPropertyIdAndPhoneE164(property.id!!, phone)
|
||||
if (existing != null) return existing
|
||||
}
|
||||
val guest = Guest(
|
||||
org = property.org,
|
||||
property = property,
|
||||
phoneE164 = phone,
|
||||
name = extracted["guestName"]?.takeIf { !it.contains("NONE", true) }
|
||||
)
|
||||
@@ -281,10 +281,6 @@ class EmailIngestionService(
|
||||
if (propertyEmails.isNotEmpty() && recipients.any { it.lowercase() in propertyEmails }) {
|
||||
return@filter true
|
||||
}
|
||||
val orgEmails = property.org.emailAliases.map { it.lowercase() }.toSet()
|
||||
if (orgEmails.isNotEmpty() && recipients.any { it.lowercase() in orgEmails }) {
|
||||
return@filter true
|
||||
}
|
||||
}
|
||||
val aliases = mutableSetOf<String>()
|
||||
aliases.add(property.name)
|
||||
|
||||
Reference in New Issue
Block a user