Compare commits
64 Commits
4998701f84
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f9fecf4a | ||
|
|
fb1c0caed7 | ||
|
|
4fdfc84811 | ||
|
|
19153900fd | ||
|
|
eb0b99f55a | ||
|
|
c3ec6e8d4a | ||
|
|
3a8f871d7d | ||
|
|
a0a9ce4d31 | ||
|
|
f9c31a4d59 | ||
|
|
236c885954 | ||
|
|
a39a9dcd1f | ||
|
|
40a09d1c83 | ||
|
|
0104e87050 | ||
|
|
1082126f86 | ||
|
|
188738e28b | ||
|
|
7f7e164acf | ||
|
|
c2c54d24f5 | ||
|
|
9aa1f71c32 | ||
|
|
ad1ef6ec0a | ||
|
|
8f6e645573 | ||
|
|
d38a29111d | ||
|
|
4f2eb3d671 | ||
|
|
8c4afb8232 | ||
|
|
be814eb0d5 | ||
|
|
3f05484498 | ||
|
|
d32c89d768 | ||
|
|
ce75e9536c | ||
|
|
0efce2f900 | ||
|
|
22a9fdc851 | ||
|
|
1400451bfe | ||
|
|
bf87d329d4 | ||
|
|
63c7479c9f | ||
|
|
721580ffd7 | ||
|
|
650e7c7354 | ||
|
|
619a48dd4f | ||
|
|
e3a7053d78 | ||
|
|
85254b229f | ||
|
|
2c337b8709 | ||
|
|
7ec8d6d350 | ||
|
|
8f47725f13 | ||
|
|
398ad93232 | ||
|
|
05b8fd409c | ||
|
|
6f961cb599 | ||
|
|
d895c4411d | ||
|
|
397bc4ede3 | ||
|
|
e1680b1991 | ||
|
|
14f739a54f | ||
|
|
3a5726203c | ||
|
|
32af3e0d82 | ||
|
|
4ec4e5e068 | ||
|
|
6963a0f252 | ||
|
|
812bb4ffb4 | ||
|
|
152bbbc575 | ||
|
|
671922b363 | ||
|
|
41299e6071 | ||
|
|
8887e456ac | ||
|
|
4f22bfa234 | ||
|
|
7b1bb55008 | ||
|
|
9ff84c2111 | ||
|
|
1f43f3274a | ||
|
|
21fd32aeee | ||
|
|
b972563971 | ||
|
|
af280ca88c | ||
|
|
e8d9590a37 |
@@ -1,32 +1,32 @@
|
|||||||
name: build
|
name: build-and-deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "**" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
|
||||||
branches: [ "**" ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-deploy:
|
||||||
runs-on: ubuntu-latest:host
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 19
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "19"
|
java-version: "21"
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Build
|
- name: Build (skip tests)
|
||||||
run: ./gradlew -q assemble
|
env:
|
||||||
|
GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.workers.max=6"
|
||||||
|
run: ./gradlew build -x test --info --stacktrace
|
||||||
|
|
||||||
deploy:
|
- name: Deploy jar and restart
|
||||||
runs-on: ubuntu-latest:host
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- name: Trigger deploy webhook
|
|
||||||
run: |
|
run: |
|
||||||
curl -sS -X POST http://127.0.0.1:9000/hooks/deploy-trisolarisserver
|
set -e
|
||||||
|
mkdir -p /opt/deploy/TrisolarisServer/build/libs
|
||||||
|
cp -f build/libs/*.jar /opt/deploy/TrisolarisServer/build/libs/
|
||||||
|
sudo systemctl restart TrisolarisServer.service
|
||||||
|
|||||||
25
AGENTS.md
25
AGENTS.md
@@ -22,7 +22,7 @@ Core principles
|
|||||||
- Room availability by room number; toAt=null means occupied.
|
- Room availability by room number; toAt=null means occupied.
|
||||||
- Room change = close old RoomStay + open new one.
|
- Room change = close old RoomStay + open new one.
|
||||||
- Multi-property: every domain object scoped to property_id.
|
- 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
|
Immutable rules
|
||||||
- Use Kotlin only; no microservices.
|
- Use Kotlin only; no microservices.
|
||||||
@@ -46,19 +46,18 @@ Security/Auth
|
|||||||
- /auth/verify and /auth/me.
|
- /auth/verify and /auth/me.
|
||||||
|
|
||||||
Domain entities
|
Domain entities
|
||||||
- Organization: name, emailAliases, allowedTransportModes.
|
|
||||||
- Property: code, name, addressText, emailAddresses, otaAliases, 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.
|
- RoomType: code/name/occupancy + otaAliases.
|
||||||
- Room: roomNumber, floor, hasNfc, active, maintenance, notes.
|
- Room: roomNumber, floor, hasNfc, active, maintenance, notes.
|
||||||
- Booking: status, expected check-in/out, emailAuditPdfUrl, transportMode, transportVehicleNumber.
|
- Booking: status, expected check-in/out, emailAuditPdfUrl, transportMode, transportVehicleNumber.
|
||||||
- Guest (org-scoped).
|
- Guest (property-scoped).
|
||||||
- RoomStay.
|
- RoomStay.
|
||||||
- RoomStayChange (idempotent room move).
|
- RoomStayChange (idempotent room move).
|
||||||
- IssuedCard (cardId, cardIndex, issuedAt, expiresAt, issuedBy, revokedAt).
|
- IssuedCard (cardId, cardIndex, issuedAt, expiresAt, issuedBy, revokedAt).
|
||||||
- PropertyCardCounter (per-property cardIndex counter).
|
- PropertyCardCounter (per-property cardIndex counter).
|
||||||
- GuestDocument (files + AI-extracted json).
|
- GuestDocument (files + AI-extracted json).
|
||||||
- GuestVehicle (org-scoped vehicle numbers).
|
- GuestVehicle (property-scoped vehicle numbers).
|
||||||
- InboundEmail (audit PDF + raw EML, extracted json, status).
|
- InboundEmail (audit PDF + raw EML, extracted json, status).
|
||||||
- RoomImage (original + thumbnail).
|
- RoomImage (original + thumbnail).
|
||||||
|
|
||||||
@@ -68,14 +67,10 @@ Auth
|
|||||||
- /auth/verify
|
- /auth/verify
|
||||||
- /auth/me
|
- /auth/me
|
||||||
|
|
||||||
Organizations / Properties / Users
|
Properties / Users
|
||||||
- POST /orgs
|
- POST /properties (creator becomes ADMIN on that property)
|
||||||
- GET /orgs/{orgId}
|
- GET /properties (super admin gets all; others get memberships)
|
||||||
- POST /orgs/{orgId}/properties
|
|
||||||
- GET /orgs/{orgId}/properties
|
|
||||||
- PUT /properties/{propertyId}
|
- PUT /properties/{propertyId}
|
||||||
- GET /orgs/{orgId}/users
|
|
||||||
- POST /orgs/{orgId}/users (removed; users created by app)
|
|
||||||
- GET /properties/{propertyId}/users
|
- GET /properties/{propertyId}/users
|
||||||
- PUT /properties/{propertyId}/users/{userId}/roles
|
- PUT /properties/{propertyId}/users/{userId}/roles
|
||||||
- DELETE /properties/{propertyId}/users/{userId} (ADMIN only)
|
- DELETE /properties/{propertyId}/users/{userId} (ADMIN only)
|
||||||
@@ -93,9 +88,8 @@ Room types
|
|||||||
- PUT /properties/{propertyId}/room-types/{roomTypeId}
|
- PUT /properties/{propertyId}/room-types/{roomTypeId}
|
||||||
- DELETE /properties/{propertyId}/room-types/{roomTypeId}
|
- DELETE /properties/{propertyId}/room-types/{roomTypeId}
|
||||||
|
|
||||||
Properties / Orgs
|
Properties
|
||||||
- Property create/update accepts addressText, otaAliases, emailAddresses, allowedTransportModes.
|
- Property create/update accepts addressText, otaAliases, emailAddresses, allowedTransportModes.
|
||||||
- Org create/get returns emailAliases + allowedTransportModes.
|
|
||||||
|
|
||||||
Booking flow
|
Booking flow
|
||||||
- /properties/{propertyId}/bookings/{bookingId}/check-in (creates RoomStay rows)
|
- /properties/{propertyId}/bookings/{bookingId}/check-in (creates RoomStay rows)
|
||||||
@@ -126,7 +120,7 @@ Room images
|
|||||||
- Thumbnails generated (320px).
|
- Thumbnails generated (320px).
|
||||||
|
|
||||||
Transport modes
|
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
|
Inbound email ingestion
|
||||||
- IMAP poller (1 min) with enable flag.
|
- IMAP poller (1 min) with enable flag.
|
||||||
@@ -153,5 +147,6 @@ Config
|
|||||||
|
|
||||||
Notes / constraints
|
Notes / constraints
|
||||||
- Users are created by app; API only manages roles.
|
- 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.
|
- Admin can assign ADMIN/MANAGER/STAFF/AGENT; Manager can assign STAFF/AGENT.
|
||||||
- Agents can only see free rooms.
|
- Agents can only see free rooms.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ description = "TrisolarisServer"
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(19)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
gradle.properties
Normal file
8
gradle.properties
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.vfs.watch=true
|
||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.jvmargs=-Xmx10g -Dfile.encoding=UTF-8
|
||||||
|
kotlin.incremental=true
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.android.trisolarisserver.component
|
package com.android.trisolarisserver.component
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
import com.android.trisolarisserver.models.property.Role
|
import com.android.trisolarisserver.models.property.Role
|
||||||
import org.springframework.security.access.AccessDeniedException
|
import org.springframework.security.access.AccessDeniedException
|
||||||
@@ -8,15 +9,30 @@ import java.util.UUID
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
class PropertyAccess(
|
class PropertyAccess(
|
||||||
private val repo: PropertyUserRepo
|
private val repo: PropertyUserRepo,
|
||||||
|
private val appUserRepo: AppUserRepo
|
||||||
) {
|
) {
|
||||||
fun requireMember(propertyId: UUID, userId: UUID) {
|
fun requireMember(propertyId: UUID, userId: UUID) {
|
||||||
if (!repo.existsByIdPropertyIdAndIdUserId(propertyId, userId))
|
val user = appUserRepo.findById(userId).orElse(null)
|
||||||
throw AccessDeniedException("No access to property")
|
if (user == null) {
|
||||||
|
throw AccessDeniedException("No access to property (user not found)")
|
||||||
|
}
|
||||||
|
if (user.superAdmin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!repo.existsByIdPropertyIdAndIdUserId(propertyId, userId)) {
|
||||||
|
throw AccessDeniedException("No access to property (not a member)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requireAnyRole(propertyId: UUID, userId: UUID, vararg roles: Role) {
|
fun requireAnyRole(propertyId: UUID, userId: UUID, vararg roles: Role) {
|
||||||
if (!repo.hasAnyRole(propertyId, userId, roles.toSet()))
|
val user = appUserRepo.findById(userId).orElse(null)
|
||||||
throw AccessDeniedException("Missing role")
|
if (user == null) {
|
||||||
|
throw AccessDeniedException("Missing role (user not found)")
|
||||||
|
}
|
||||||
|
if (user.superAdmin) return
|
||||||
|
if (!repo.hasAnyRole(propertyId, userId, roles.toSet())) {
|
||||||
|
throw AccessDeniedException("Missing role (no matching roles)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.android.trisolarisserver.config
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.AccessDeniedException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class ApiExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(ResponseStatusException::class)
|
||||||
|
fun handleResponseStatus(
|
||||||
|
ex: ResponseStatusException,
|
||||||
|
request: HttpServletRequest
|
||||||
|
): ResponseEntity<ApiError> {
|
||||||
|
val status = ex.statusCode as HttpStatus
|
||||||
|
return ResponseEntity.status(status).body(
|
||||||
|
ApiError(
|
||||||
|
timestamp = OffsetDateTime.now().toString(),
|
||||||
|
status = status.value(),
|
||||||
|
error = status.reasonPhrase,
|
||||||
|
message = ex.reason ?: "Request failed",
|
||||||
|
path = request.requestURI
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AccessDeniedException::class)
|
||||||
|
fun handleAccessDenied(
|
||||||
|
ex: AccessDeniedException,
|
||||||
|
request: HttpServletRequest
|
||||||
|
): ResponseEntity<ApiError> {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(
|
||||||
|
ApiError(
|
||||||
|
timestamp = OffsetDateTime.now().toString(),
|
||||||
|
status = HttpStatus.FORBIDDEN.value(),
|
||||||
|
error = HttpStatus.FORBIDDEN.reasonPhrase,
|
||||||
|
message = ex.message ?: "Forbidden",
|
||||||
|
path = request.requestURI
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception::class)
|
||||||
|
fun handleGeneric(
|
||||||
|
ex: Exception,
|
||||||
|
request: HttpServletRequest
|
||||||
|
): ResponseEntity<ApiError> {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
|
||||||
|
ApiError(
|
||||||
|
timestamp = OffsetDateTime.now().toString(),
|
||||||
|
status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||||
|
error = HttpStatus.INTERNAL_SERVER_ERROR.reasonPhrase,
|
||||||
|
message = ex.message ?: "Internal server error",
|
||||||
|
path = request.requestURI
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ApiError(
|
||||||
|
val timestamp: String,
|
||||||
|
val status: Int,
|
||||||
|
val error: String,
|
||||||
|
val message: String,
|
||||||
|
val path: String
|
||||||
|
)
|
||||||
@@ -5,13 +5,20 @@ import com.android.trisolarisserver.controller.dto.UserResponse
|
|||||||
import com.android.trisolarisserver.repo.AppUserRepo
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import org.springframework.web.server.ResponseStatusException
|
import org.springframework.web.server.ResponseStatusException
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
@@ -19,21 +26,49 @@ class Auth(
|
|||||||
private val appUserRepo: AppUserRepo,
|
private val appUserRepo: AppUserRepo,
|
||||||
private val propertyUserRepo: PropertyUserRepo
|
private val propertyUserRepo: PropertyUserRepo
|
||||||
) {
|
) {
|
||||||
|
private val logger = LoggerFactory.getLogger(Auth::class.java)
|
||||||
|
|
||||||
@PostMapping("/verify")
|
@PostMapping("/verify")
|
||||||
fun verify(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
|
fun verify(
|
||||||
return buildAuthResponse(principal)
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
request: HttpServletRequest
|
||||||
|
): ResponseEntity<AuthResponse> {
|
||||||
|
logger.info("Auth verify hit, principalPresent={}", principal != null)
|
||||||
|
val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request)
|
||||||
|
return resolved.toResponseEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
fun me(@AuthenticationPrincipal principal: MyPrincipal?): AuthResponse {
|
fun me(
|
||||||
return buildAuthResponse(principal)
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
request: HttpServletRequest
|
||||||
|
): ResponseEntity<AuthResponse> {
|
||||||
|
val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request)
|
||||||
|
return resolved.toResponseEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildAuthResponse(principal: MyPrincipal?): AuthResponse {
|
@PutMapping("/me")
|
||||||
if (principal == null) {
|
fun updateMe(
|
||||||
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
request: HttpServletRequest,
|
||||||
|
@RequestBody body: UpdateMeRequest
|
||||||
|
): ResponseEntity<AuthResponse> {
|
||||||
|
val resolved = principal?.let { ResolveResult(it) } ?: resolvePrincipalFromHeader(request)
|
||||||
|
if (resolved.principal == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(AuthResponse(status = "UNAUTHORIZED"))
|
||||||
}
|
}
|
||||||
|
val user = appUserRepo.findById(resolved.principal.userId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||||
|
}
|
||||||
|
if (!body.name.isNullOrBlank()) {
|
||||||
|
user.name = body.name.trim()
|
||||||
|
}
|
||||||
|
appUserRepo.save(user)
|
||||||
|
return ResponseEntity.ok(buildAuthResponse(resolved.principal))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthResponse(principal: MyPrincipal): AuthResponse {
|
||||||
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||||
}
|
}
|
||||||
@@ -44,21 +79,85 @@ class Auth(
|
|||||||
roles = it.roles.map { role -> role.name }.toSet()
|
roles = it.roles.map { role -> role.name }.toSet()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val status = when {
|
||||||
|
user.superAdmin -> "SUPER_ADMIN"
|
||||||
|
memberships.isEmpty() -> "NO_PROPERTIES"
|
||||||
|
else -> "OK"
|
||||||
|
}
|
||||||
return AuthResponse(
|
return AuthResponse(
|
||||||
|
status = status,
|
||||||
user = UserResponse(
|
user = UserResponse(
|
||||||
id = user.id!!,
|
id = user.id!!,
|
||||||
orgId = user.org.id!!,
|
|
||||||
firebaseUid = user.firebaseUid,
|
firebaseUid = user.firebaseUid,
|
||||||
phoneE164 = user.phoneE164,
|
phoneE164 = user.phoneE164,
|
||||||
name = user.name,
|
name = user.name,
|
||||||
disabled = user.disabled
|
disabled = user.disabled,
|
||||||
|
superAdmin = user.superAdmin
|
||||||
),
|
),
|
||||||
properties = memberships
|
properties = memberships
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolvePrincipalFromHeader(request: HttpServletRequest): ResolveResult {
|
||||||
|
val header = request.getHeader("Authorization") ?: throw ResponseStatusException(
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
"Missing Authorization token"
|
||||||
|
)
|
||||||
|
if (!header.startsWith("Bearer ")) {
|
||||||
|
logger.warn("Auth verify invalid Authorization header")
|
||||||
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid Authorization header")
|
||||||
|
}
|
||||||
|
val token = header.removePrefix("Bearer ").trim()
|
||||||
|
val decoded = try {
|
||||||
|
FirebaseAuth.getInstance().verifyIdToken(token)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
logger.warn("Auth verify failed: {}", ex.message)
|
||||||
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token")
|
||||||
|
}
|
||||||
|
val user = appUserRepo.findByFirebaseUid(decoded.uid) ?: run {
|
||||||
|
val phone = decoded.claims["phone_number"] as? String
|
||||||
|
val name = decoded.claims["name"] as? String
|
||||||
|
val makeSuperAdmin = appUserRepo.count() == 0L
|
||||||
|
val created = appUserRepo.save(
|
||||||
|
com.android.trisolarisserver.models.property.AppUser(
|
||||||
|
firebaseUid = decoded.uid,
|
||||||
|
phoneE164 = phone,
|
||||||
|
name = name,
|
||||||
|
superAdmin = makeSuperAdmin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.warn("Auth verify auto-created user uid={}, userId={}", decoded.uid, created.id)
|
||||||
|
created
|
||||||
|
}
|
||||||
|
logger.warn("Auth verify resolved uid={}, userId={}", decoded.uid, user.id)
|
||||||
|
return ResolveResult(
|
||||||
|
MyPrincipal(
|
||||||
|
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
|
||||||
|
firebaseUid = decoded.uid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ResolveResult.toResponseEntity(): ResponseEntity<AuthResponse> {
|
||||||
|
return if (principal == null) {
|
||||||
|
ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(AuthResponse(status = "UNAUTHORIZED"))
|
||||||
|
} else {
|
||||||
|
ResponseEntity.ok(buildAuthResponse(principal))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AuthResponse(
|
data class AuthResponse(
|
||||||
val user: UserResponse,
|
val status: String,
|
||||||
val properties: List<PropertyUserResponse>
|
val user: UserResponse? = null,
|
||||||
|
val properties: List<PropertyUserResponse> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateMeRequest(
|
||||||
|
val name: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ResolveResult(
|
||||||
|
val principal: MyPrincipal?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -261,10 +261,10 @@ class BookingFlow(
|
|||||||
property: com.android.trisolarisserver.models.property.Property,
|
property: com.android.trisolarisserver.models.property.Property,
|
||||||
mode: TransportMode
|
mode: TransportMode
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val allowed = when {
|
val allowed = if (property.allowedTransportModes.isNotEmpty()) {
|
||||||
property.allowedTransportModes.isNotEmpty() -> property.allowedTransportModes
|
property.allowedTransportModes
|
||||||
property.org.allowedTransportModes.isNotEmpty() -> property.org.allowedTransportModes
|
} else {
|
||||||
else -> TransportMode.entries.toSet()
|
TransportMode.entries.toSet()
|
||||||
}
|
}
|
||||||
return allowed.contains(mode)
|
return allowed.contains(mode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class GuestDocuments(
|
|||||||
val guest = guestRepo.findById(guestId).orElseThrow {
|
val guest = guestRepo.findById(guestId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
||||||
}
|
}
|
||||||
if (guest.org.id != property.org.id) {
|
if (guest.property.id != property.id) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property org")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||||
}
|
}
|
||||||
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
val booking = bookingRepo.findById(bookingId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Booking not found")
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ class GuestRatings(
|
|||||||
val guest = guestRepo.findById(guestId).orElseThrow {
|
val guest = guestRepo.findById(guestId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
||||||
}
|
}
|
||||||
if (guest.org.id != property.org.id) {
|
if (guest.property.id != property.id) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property org")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||||
}
|
}
|
||||||
|
|
||||||
val booking = bookingRepo.findById(request.bookingId).orElseThrow {
|
val booking = bookingRepo.findById(request.bookingId).orElseThrow {
|
||||||
@@ -70,7 +70,6 @@ class GuestRatings(
|
|||||||
|
|
||||||
val score = parseScore(request.score)
|
val score = parseScore(request.score)
|
||||||
val rating = GuestRating(
|
val rating = GuestRating(
|
||||||
org = property.org,
|
|
||||||
property = property,
|
property = property,
|
||||||
guest = guest,
|
guest = guest,
|
||||||
booking = booking,
|
booking = booking,
|
||||||
@@ -97,8 +96,8 @@ class GuestRatings(
|
|||||||
val guest = guestRepo.findById(guestId).orElseThrow {
|
val guest = guestRepo.findById(guestId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
||||||
}
|
}
|
||||||
if (guest.org.id != property.org.id) {
|
if (guest.property.id != property.id) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property org")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property")
|
||||||
}
|
}
|
||||||
|
|
||||||
return guestRatingRepo.findByGuestIdOrderByCreatedAtDesc(guestId).map { it.toResponse() }
|
return guestRatingRepo.findByGuestIdOrderByCreatedAtDesc(guestId).map { it.toResponse() }
|
||||||
@@ -117,7 +116,6 @@ class GuestRatings(
|
|||||||
private fun GuestRating.toResponse(): GuestRatingResponse {
|
private fun GuestRating.toResponse(): GuestRatingResponse {
|
||||||
return GuestRatingResponse(
|
return GuestRatingResponse(
|
||||||
id = id!!,
|
id = id!!,
|
||||||
orgId = org.id!!,
|
|
||||||
propertyId = property.id!!,
|
propertyId = property.id!!,
|
||||||
guestId = guest.id!!,
|
guestId = guest.id!!,
|
||||||
bookingId = booking.id!!,
|
bookingId = booking.id!!,
|
||||||
|
|||||||
@@ -43,15 +43,14 @@ class Guests(
|
|||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
}
|
}
|
||||||
val orgId = property.org.id ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Org missing")
|
|
||||||
|
|
||||||
val guests = mutableSetOf<Guest>()
|
val guests = mutableSetOf<Guest>()
|
||||||
if (!phone.isNullOrBlank()) {
|
if (!phone.isNullOrBlank()) {
|
||||||
val guest = guestRepo.findByOrgIdAndPhoneE164(orgId, phone)
|
val guest = guestRepo.findByPropertyIdAndPhoneE164(propertyId, phone)
|
||||||
if (guest != null) guests.add(guest)
|
if (guest != null) guests.add(guest)
|
||||||
}
|
}
|
||||||
if (!vehicleNumber.isNullOrBlank()) {
|
if (!vehicleNumber.isNullOrBlank()) {
|
||||||
val vehicle = guestVehicleRepo.findByOrgIdAndVehicleNumberIgnoreCase(orgId, vehicleNumber)
|
val vehicle = guestVehicleRepo.findByPropertyIdAndVehicleNumberIgnoreCase(propertyId, vehicleNumber)
|
||||||
if (vehicle != null) guests.add(vehicle.guest)
|
if (vehicle != null) guests.add(vehicle.guest)
|
||||||
}
|
}
|
||||||
return guests.toResponse(guestVehicleRepo, guestRatingRepo)
|
return guests.toResponse(guestVehicleRepo, guestRatingRepo)
|
||||||
@@ -74,15 +73,15 @@ class Guests(
|
|||||||
val guest = guestRepo.findById(guestId).orElseThrow {
|
val guest = guestRepo.findById(guestId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Guest not found")
|
||||||
}
|
}
|
||||||
if (guest.org.id != property.org.id) {
|
if (guest.property.id != property.id) {
|
||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Guest not in property org")
|
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")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Vehicle number already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
val vehicle = GuestVehicle(
|
val vehicle = GuestVehicle(
|
||||||
org = property.org,
|
property = property,
|
||||||
guest = guest,
|
guest = guest,
|
||||||
vehicleNumber = request.vehicleNumber.trim()
|
vehicleNumber = request.vehicleNumber.trim()
|
||||||
)
|
)
|
||||||
@@ -116,7 +115,6 @@ private fun Set<Guest>.toResponse(
|
|||||||
return this.map { guest ->
|
return this.map { guest ->
|
||||||
GuestResponse(
|
GuestResponse(
|
||||||
id = guest.id!!,
|
id = guest.id!!,
|
||||||
orgId = guest.org.id!!,
|
|
||||||
name = guest.name,
|
name = guest.name,
|
||||||
phoneE164 = guest.phoneE164,
|
phoneE164 = guest.phoneE164,
|
||||||
nationality = guest.nationality,
|
nationality = guest.nationality,
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
class Health {
|
class Health {
|
||||||
@GetMapping("/health")
|
@GetMapping("/health")
|
||||||
fun health(): Map<String, String> {
|
fun health(): Map<String, String> {
|
||||||
return mapOf("status" to "ok")
|
return mapOf("status" to "ok", "build" to "2026-01-26-authfix")
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
fun root(): Map<String, String> {
|
fun root(): Map<String, String> {
|
||||||
return mapOf("status" to "ok")
|
return mapOf("status" to "ok", "build" to "2026-01-26-authfix")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.PropertyUpdateRequest
|
||||||
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
|
import com.android.trisolarisserver.controller.dto.PropertyUserResponse
|
||||||
import com.android.trisolarisserver.controller.dto.PropertyUserRoleRequest
|
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.AppUserRepo
|
||||||
import com.android.trisolarisserver.repo.OrganizationRepo
|
|
||||||
import com.android.trisolarisserver.repo.PropertyRepo
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
import com.android.trisolarisserver.repo.PropertyUserRepo
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
import com.android.trisolarisserver.models.property.Property
|
import com.android.trisolarisserver.models.property.Property
|
||||||
@@ -34,33 +32,22 @@ import java.util.UUID
|
|||||||
class Properties(
|
class Properties(
|
||||||
private val propertyAccess: PropertyAccess,
|
private val propertyAccess: PropertyAccess,
|
||||||
private val propertyRepo: PropertyRepo,
|
private val propertyRepo: PropertyRepo,
|
||||||
private val orgRepo: OrganizationRepo,
|
|
||||||
private val propertyUserRepo: PropertyUserRepo,
|
private val propertyUserRepo: PropertyUserRepo,
|
||||||
private val appUserRepo: AppUserRepo
|
private val appUserRepo: AppUserRepo
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PostMapping("/orgs/{orgId}/properties")
|
@PostMapping("/properties")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
fun createProperty(
|
fun createProperty(
|
||||||
@PathVariable orgId: UUID,
|
|
||||||
@AuthenticationPrincipal principal: MyPrincipal?,
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
@RequestBody request: PropertyCreateRequest
|
@RequestBody request: PropertyCreateRequest
|
||||||
): PropertyResponse {
|
): PropertyResponse {
|
||||||
val user = requireUser(principal)
|
val user = requireUser(principal)
|
||||||
if (user.org.id != orgId) {
|
if (propertyRepo.existsByCode(request.code)) {
|
||||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists")
|
||||||
}
|
|
||||||
requireOrgRole(orgId, user.id!!, Role.ADMIN)
|
|
||||||
|
|
||||||
if (propertyRepo.existsByOrgIdAndCode(orgId, request.code)) {
|
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val org = orgRepo.findById(orgId).orElseThrow {
|
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Org not found")
|
|
||||||
}
|
|
||||||
val property = Property(
|
val property = Property(
|
||||||
org = org,
|
|
||||||
code = request.code,
|
code = request.code,
|
||||||
name = request.name,
|
name = request.name,
|
||||||
addressText = request.addressText,
|
addressText = request.addressText,
|
||||||
@@ -72,33 +59,34 @@ class Properties(
|
|||||||
allowedTransportModes = request.allowedTransportModes?.let { parseTransportModes(it) } ?: mutableSetOf()
|
allowedTransportModes = request.allowedTransportModes?.let { parseTransportModes(it) } ?: mutableSetOf()
|
||||||
)
|
)
|
||||||
val saved = propertyRepo.save(property)
|
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()
|
return saved.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/orgs/{orgId}/properties")
|
@GetMapping("/properties")
|
||||||
fun listProperties(
|
fun listProperties(
|
||||||
@PathVariable orgId: UUID,
|
|
||||||
@AuthenticationPrincipal principal: MyPrincipal?
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
): List<PropertyResponse> {
|
): List<PropertyResponse> {
|
||||||
val user = requireUser(principal)
|
val user = requireUser(principal)
|
||||||
if (user.org.id != orgId) {
|
return if (user.superAdmin) {
|
||||||
throw ResponseStatusException(HttpStatus.FORBIDDEN, "No access to org")
|
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")
|
@GetMapping("/properties/{propertyId}/users")
|
||||||
@@ -129,8 +117,10 @@ class Properties(
|
|||||||
requirePrincipal(principal)
|
requirePrincipal(principal)
|
||||||
propertyAccess.requireMember(propertyId, principal!!.userId)
|
propertyAccess.requireMember(propertyId, principal!!.userId)
|
||||||
|
|
||||||
|
val actorUser = appUserRepo.findById(principal.userId).orElse(null)
|
||||||
val actorRoles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
|
val actorRoles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
|
||||||
val allowedRoles = when {
|
val allowedRoles = when {
|
||||||
|
actorUser?.superAdmin == true -> setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT)
|
||||||
actorRoles.contains(Role.ADMIN) -> setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT)
|
actorRoles.contains(Role.ADMIN) -> setOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT)
|
||||||
actorRoles.contains(Role.MANAGER) -> setOf(Role.STAFF, Role.AGENT)
|
actorRoles.contains(Role.MANAGER) -> setOf(Role.STAFF, Role.AGENT)
|
||||||
else -> emptySet()
|
else -> emptySet()
|
||||||
@@ -153,9 +143,6 @@ class Properties(
|
|||||||
val targetUser = appUserRepo.findById(userId).orElseThrow {
|
val targetUser = appUserRepo.findById(userId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
|
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(
|
val propertyUser = PropertyUser(
|
||||||
id = PropertyUserId(propertyId = propertyId, userId = userId),
|
id = PropertyUserId(propertyId = propertyId, userId = userId),
|
||||||
@@ -201,8 +188,8 @@ class Properties(
|
|||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
}
|
}
|
||||||
if (propertyRepo.existsByOrgIdAndCodeAndIdNot(property.org.id!!, request.code, propertyId)) {
|
if (propertyRepo.existsByCodeAndIdNot(request.code, propertyId)) {
|
||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists for org")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Property code already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
property.code = request.code
|
property.code = request.code
|
||||||
@@ -239,12 +226,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> {
|
private fun parseTransportModes(modes: Set<String>): MutableSet<TransportMode> {
|
||||||
return try {
|
return try {
|
||||||
modes.map { TransportMode.valueOf(it) }.toMutableSet()
|
modes.map { TransportMode.valueOf(it) }.toMutableSet()
|
||||||
@@ -256,10 +237,8 @@ class Properties(
|
|||||||
|
|
||||||
private fun Property.toResponse(): PropertyResponse {
|
private fun Property.toResponse(): PropertyResponse {
|
||||||
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Property id missing")
|
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(
|
return PropertyResponse(
|
||||||
id = id,
|
id = id,
|
||||||
orgId = orgId,
|
|
||||||
code = code,
|
code = code,
|
||||||
name = name,
|
name = name,
|
||||||
addressText = addressText,
|
addressText = addressText,
|
||||||
@@ -271,16 +250,3 @@ private fun Property.toResponse(): PropertyResponse {
|
|||||||
allowedTransportModes = allowedTransportModes.map { it.name }.toSet()
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.controller.dto.AmenityResponse
|
||||||
|
import com.android.trisolarisserver.controller.dto.AmenityUpsertRequest
|
||||||
|
import com.android.trisolarisserver.models.room.RoomAmenity
|
||||||
|
import com.android.trisolarisserver.repo.AppUserRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomAmenityRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomTypeRepo
|
||||||
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
|
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.PutMapping
|
||||||
|
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("/amenities")
|
||||||
|
class RoomAmenities(
|
||||||
|
private val roomAmenityRepo: RoomAmenityRepo,
|
||||||
|
private val roomTypeRepo: RoomTypeRepo,
|
||||||
|
private val appUserRepo: AppUserRepo
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun listAmenities(
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): List<AmenityResponse> {
|
||||||
|
requirePrincipal(principal)
|
||||||
|
return roomAmenityRepo.findAllByOrderByName().map { it.toResponse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
fun createAmenity(
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: AmenityUpsertRequest
|
||||||
|
): AmenityResponse {
|
||||||
|
requireSuperAdmin(principal)
|
||||||
|
|
||||||
|
if (roomAmenityRepo.existsByName(request.name)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity already exists")
|
||||||
|
}
|
||||||
|
val amenity = RoomAmenity(
|
||||||
|
name = request.name,
|
||||||
|
category = request.category,
|
||||||
|
iconKey = request.iconKey
|
||||||
|
)
|
||||||
|
return roomAmenityRepo.save(amenity).toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{amenityId}")
|
||||||
|
fun updateAmenity(
|
||||||
|
@PathVariable amenityId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?,
|
||||||
|
@RequestBody request: AmenityUpsertRequest
|
||||||
|
): AmenityResponse {
|
||||||
|
requireSuperAdmin(principal)
|
||||||
|
|
||||||
|
val amenity = roomAmenityRepo.findById(amenityId).orElse(null)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
|
||||||
|
|
||||||
|
if (roomAmenityRepo.existsByNameAndIdNot(request.name, amenityId)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
amenity.name = request.name
|
||||||
|
amenity.category = request.category ?: amenity.category
|
||||||
|
amenity.iconKey = request.iconKey ?: amenity.iconKey
|
||||||
|
return roomAmenityRepo.save(amenity).toResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{amenityId}")
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
fun deleteAmenity(
|
||||||
|
@PathVariable amenityId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
) {
|
||||||
|
requireSuperAdmin(principal)
|
||||||
|
|
||||||
|
val amenity = roomAmenityRepo.findById(amenityId).orElse(null)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
|
||||||
|
|
||||||
|
if (roomTypeRepo.existsByAmenitiesId(amenityId)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Amenity is used by room types")
|
||||||
|
}
|
||||||
|
roomAmenityRepo.delete(amenity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||||
|
if (principal == null) {
|
||||||
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireSuperAdmin(principal: MyPrincipal?) {
|
||||||
|
if (principal == null) {
|
||||||
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
|
}
|
||||||
|
val user = appUserRepo.findById(principal.userId).orElseThrow {
|
||||||
|
ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||||
|
}
|
||||||
|
if (!user.superAdmin) {
|
||||||
|
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Super admin only")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomAmenity.toResponse(): AmenityResponse {
|
||||||
|
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Amenity id missing")
|
||||||
|
return AmenityResponse(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
category = category,
|
||||||
|
iconKey = iconKey
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.android.trisolarisserver.controller
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.component.PropertyAccess
|
||||||
|
import com.android.trisolarisserver.controller.dto.ActiveRoomStayResponse
|
||||||
|
import com.android.trisolarisserver.models.property.Role
|
||||||
|
import com.android.trisolarisserver.repo.PropertyUserRepo
|
||||||
|
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.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class RoomStays(
|
||||||
|
private val propertyAccess: PropertyAccess,
|
||||||
|
private val propertyUserRepo: PropertyUserRepo,
|
||||||
|
private val roomStayRepo: RoomStayRepo
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping("/properties/{propertyId}/room-stays/active")
|
||||||
|
fun listActiveRoomStays(
|
||||||
|
@PathVariable propertyId: UUID,
|
||||||
|
@AuthenticationPrincipal principal: MyPrincipal?
|
||||||
|
): List<ActiveRoomStayResponse> {
|
||||||
|
if (principal == null) {
|
||||||
|
throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing principal")
|
||||||
|
}
|
||||||
|
propertyAccess.requireMember(propertyId, principal.userId)
|
||||||
|
|
||||||
|
val roles = propertyUserRepo.findRolesByPropertyAndUser(propertyId, principal.userId)
|
||||||
|
if (isAgentOnly(roles)) {
|
||||||
|
throw ResponseStatusException(HttpStatus.FORBIDDEN, "Agents cannot view active stays")
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomStayRepo.findActiveByPropertyIdWithDetails(propertyId).map { stay ->
|
||||||
|
val booking = stay.booking
|
||||||
|
val guest = booking.primaryGuest
|
||||||
|
val room = stay.room
|
||||||
|
val roomType = room.roomType
|
||||||
|
ActiveRoomStayResponse(
|
||||||
|
roomStayId = stay.id!!,
|
||||||
|
bookingId = booking.id!!,
|
||||||
|
guestId = guest?.id,
|
||||||
|
guestName = guest?.name,
|
||||||
|
guestPhone = guest?.phoneE164,
|
||||||
|
roomId = room.id!!,
|
||||||
|
roomNumber = room.roomNumber,
|
||||||
|
roomTypeName = roomType.name,
|
||||||
|
fromAt = stay.fromAt.toString(),
|
||||||
|
checkinAt = booking.checkinAt?.toString(),
|
||||||
|
expectedCheckoutAt = booking.expectedCheckoutAt?.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAgentOnly(roles: Set<Role>): Boolean {
|
||||||
|
if (!roles.contains(Role.AGENT)) return false
|
||||||
|
val privileged = setOf(
|
||||||
|
Role.ADMIN,
|
||||||
|
Role.MANAGER,
|
||||||
|
Role.STAFF,
|
||||||
|
Role.HOUSEKEEPING,
|
||||||
|
Role.FINANCE,
|
||||||
|
Role.SUPERVISOR,
|
||||||
|
Role.GUIDE
|
||||||
|
)
|
||||||
|
return roles.none { it in privileged }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import com.android.trisolarisserver.component.PropertyAccess
|
|||||||
import com.android.trisolarisserver.controller.dto.RoomTypeResponse
|
import com.android.trisolarisserver.controller.dto.RoomTypeResponse
|
||||||
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
|
import com.android.trisolarisserver.controller.dto.RoomTypeUpsertRequest
|
||||||
import com.android.trisolarisserver.repo.PropertyRepo
|
import com.android.trisolarisserver.repo.PropertyRepo
|
||||||
|
import com.android.trisolarisserver.repo.RoomAmenityRepo
|
||||||
import com.android.trisolarisserver.repo.RoomRepo
|
import com.android.trisolarisserver.repo.RoomRepo
|
||||||
import com.android.trisolarisserver.repo.RoomTypeRepo
|
import com.android.trisolarisserver.repo.RoomTypeRepo
|
||||||
import com.android.trisolarisserver.models.property.Role
|
import com.android.trisolarisserver.models.property.Role
|
||||||
|
import com.android.trisolarisserver.models.room.RoomAmenity
|
||||||
import com.android.trisolarisserver.models.room.RoomType
|
import com.android.trisolarisserver.models.room.RoomType
|
||||||
import com.android.trisolarisserver.security.MyPrincipal
|
import com.android.trisolarisserver.security.MyPrincipal
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
@@ -28,6 +30,7 @@ import java.util.UUID
|
|||||||
class RoomTypes(
|
class RoomTypes(
|
||||||
private val propertyAccess: PropertyAccess,
|
private val propertyAccess: PropertyAccess,
|
||||||
private val roomTypeRepo: RoomTypeRepo,
|
private val roomTypeRepo: RoomTypeRepo,
|
||||||
|
private val roomAmenityRepo: RoomAmenityRepo,
|
||||||
private val roomRepo: RoomRepo,
|
private val roomRepo: RoomRepo,
|
||||||
private val propertyRepo: PropertyRepo
|
private val propertyRepo: PropertyRepo
|
||||||
) {
|
) {
|
||||||
@@ -66,11 +69,27 @@ class RoomTypes(
|
|||||||
name = request.name,
|
name = request.name,
|
||||||
baseOccupancy = request.baseOccupancy ?: 2,
|
baseOccupancy = request.baseOccupancy ?: 2,
|
||||||
maxOccupancy = request.maxOccupancy ?: 3,
|
maxOccupancy = request.maxOccupancy ?: 3,
|
||||||
|
sqFeet = request.sqFeet,
|
||||||
|
bathroomSqFeet = request.bathroomSqFeet,
|
||||||
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
otaAliases = request.otaAliases?.toMutableSet() ?: mutableSetOf()
|
||||||
)
|
)
|
||||||
|
if (request.amenityIds != null) {
|
||||||
|
roomType.amenities = resolveAmenities(request.amenityIds)
|
||||||
|
}
|
||||||
return roomTypeRepo.save(roomType).toResponse()
|
return roomTypeRepo.save(roomType).toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveAmenities(ids: Set<UUID>): MutableSet<RoomAmenity> {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return mutableSetOf()
|
||||||
|
}
|
||||||
|
val amenities = roomAmenityRepo.findByIdIn(ids)
|
||||||
|
if (amenities.size != ids.size) {
|
||||||
|
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Amenity not found")
|
||||||
|
}
|
||||||
|
return amenities.toMutableSet()
|
||||||
|
}
|
||||||
|
|
||||||
@PutMapping("/{roomTypeId}")
|
@PutMapping("/{roomTypeId}")
|
||||||
fun updateRoomType(
|
fun updateRoomType(
|
||||||
@PathVariable propertyId: UUID,
|
@PathVariable propertyId: UUID,
|
||||||
@@ -93,9 +112,14 @@ class RoomTypes(
|
|||||||
roomType.name = request.name
|
roomType.name = request.name
|
||||||
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
|
roomType.baseOccupancy = request.baseOccupancy ?: roomType.baseOccupancy
|
||||||
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
roomType.maxOccupancy = request.maxOccupancy ?: roomType.maxOccupancy
|
||||||
|
roomType.sqFeet = request.sqFeet ?: roomType.sqFeet
|
||||||
|
roomType.bathroomSqFeet = request.bathroomSqFeet ?: roomType.bathroomSqFeet
|
||||||
if (request.otaAliases != null) {
|
if (request.otaAliases != null) {
|
||||||
roomType.otaAliases = request.otaAliases.toMutableSet()
|
roomType.otaAliases = request.otaAliases.toMutableSet()
|
||||||
}
|
}
|
||||||
|
if (request.amenityIds != null) {
|
||||||
|
roomType.amenities = resolveAmenities(request.amenityIds)
|
||||||
|
}
|
||||||
return roomTypeRepo.save(roomType).toResponse()
|
return roomTypeRepo.save(roomType).toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +160,19 @@ private fun RoomType.toResponse(): RoomTypeResponse {
|
|||||||
name = name,
|
name = name,
|
||||||
baseOccupancy = baseOccupancy,
|
baseOccupancy = baseOccupancy,
|
||||||
maxOccupancy = maxOccupancy,
|
maxOccupancy = maxOccupancy,
|
||||||
otaAliases = otaAliases.toSet()
|
sqFeet = sqFeet,
|
||||||
|
bathroomSqFeet = bathroomSqFeet,
|
||||||
|
otaAliases = otaAliases.toSet(),
|
||||||
|
amenities = amenities.map { it.toResponse() }.toSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun RoomAmenity.toResponse(): com.android.trisolarisserver.controller.dto.AmenityResponse {
|
||||||
|
val id = id ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Amenity id missing")
|
||||||
|
return com.android.trisolarisserver.controller.dto.AmenityResponse(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
category = category,
|
||||||
|
iconKey = iconKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,8 +176,7 @@ class Rooms(
|
|||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
ResponseStatusException(HttpStatus.NOT_FOUND, "Property not found")
|
||||||
}
|
}
|
||||||
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
|
val roomType = resolveRoomType(propertyId, request)
|
||||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
|
||||||
|
|
||||||
val room = Room(
|
val room = Room(
|
||||||
property = property,
|
property = property,
|
||||||
@@ -190,9 +189,19 @@ class Rooms(
|
|||||||
notes = request.notes
|
notes = request.notes
|
||||||
)
|
)
|
||||||
|
|
||||||
val saved = roomRepo.save(room).toRoomResponse()
|
val saved = roomRepo.save(room)
|
||||||
|
val response = RoomResponse(
|
||||||
|
id = saved.id ?: throw IllegalStateException("Room id is null"),
|
||||||
|
roomNumber = saved.roomNumber,
|
||||||
|
floor = saved.floor,
|
||||||
|
roomTypeName = roomType.name,
|
||||||
|
hasNfc = saved.hasNfc,
|
||||||
|
active = saved.active,
|
||||||
|
maintenance = saved.maintenance,
|
||||||
|
notes = saved.notes
|
||||||
|
)
|
||||||
roomBoardEvents.emit(propertyId)
|
roomBoardEvents.emit(propertyId)
|
||||||
return saved
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{roomId}")
|
@PutMapping("/{roomId}")
|
||||||
@@ -212,8 +221,7 @@ class Rooms(
|
|||||||
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
|
throw ResponseStatusException(HttpStatus.CONFLICT, "Room number already exists for property")
|
||||||
}
|
}
|
||||||
|
|
||||||
val roomType = roomTypeRepo.findByIdAndPropertyId(request.roomTypeId, propertyId)
|
val roomType = resolveRoomType(propertyId, request)
|
||||||
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
|
||||||
|
|
||||||
room.roomNumber = request.roomNumber
|
room.roomNumber = request.roomNumber
|
||||||
room.floor = request.floor
|
room.floor = request.floor
|
||||||
@@ -223,9 +231,19 @@ class Rooms(
|
|||||||
room.maintenance = request.maintenance
|
room.maintenance = request.maintenance
|
||||||
room.notes = request.notes
|
room.notes = request.notes
|
||||||
|
|
||||||
val saved = roomRepo.save(room).toRoomResponse()
|
val saved = roomRepo.save(room)
|
||||||
|
val response = RoomResponse(
|
||||||
|
id = saved.id ?: throw IllegalStateException("Room id is null"),
|
||||||
|
roomNumber = saved.roomNumber,
|
||||||
|
floor = saved.floor,
|
||||||
|
roomTypeName = roomType.name,
|
||||||
|
hasNfc = saved.hasNfc,
|
||||||
|
active = saved.active,
|
||||||
|
maintenance = saved.maintenance,
|
||||||
|
notes = saved.notes
|
||||||
|
)
|
||||||
roomBoardEvents.emit(propertyId)
|
roomBoardEvents.emit(propertyId)
|
||||||
return saved
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requirePrincipal(principal: MyPrincipal?) {
|
private fun requirePrincipal(principal: MyPrincipal?) {
|
||||||
@@ -247,16 +265,23 @@ class Rooms(
|
|||||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date format")
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid date format")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveRoomType(propertyId: UUID, request: RoomUpsertRequest): com.android.trisolarisserver.models.room.RoomType {
|
||||||
|
val code = request.roomTypeCode.trim()
|
||||||
|
if (code.isBlank()) {
|
||||||
|
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "roomTypeCode required")
|
||||||
|
}
|
||||||
|
return roomTypeRepo.findByPropertyIdAndCodeIgnoreCase(propertyId, code)
|
||||||
|
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Room type not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Room.toRoomResponse(): RoomResponse {
|
private fun Room.toRoomResponse(): RoomResponse {
|
||||||
val roomId = id ?: throw IllegalStateException("Room id is null")
|
val roomId = id ?: throw IllegalStateException("Room id is null")
|
||||||
val roomTypeId = roomType.id ?: throw IllegalStateException("Room type id is null")
|
|
||||||
return RoomResponse(
|
return RoomResponse(
|
||||||
id = roomId,
|
id = roomId,
|
||||||
roomNumber = roomNumber,
|
roomNumber = roomNumber,
|
||||||
floor = floor,
|
floor = floor,
|
||||||
roomTypeId = roomTypeId,
|
|
||||||
roomTypeName = roomType.name,
|
roomTypeName = roomType.name,
|
||||||
hasNfc = hasNfc,
|
hasNfc = hasNfc,
|
||||||
active = active,
|
active = active,
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ class TransportModes(
|
|||||||
val property = propertyRepo.findById(propertyId).orElseThrow {
|
val property = propertyRepo.findById(propertyId).orElseThrow {
|
||||||
ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "Property not found")
|
ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "Property not found")
|
||||||
}
|
}
|
||||||
val allowed = when {
|
val allowed = if (property.allowedTransportModes.isNotEmpty()) {
|
||||||
property.allowedTransportModes.isNotEmpty() -> property.allowedTransportModes
|
property.allowedTransportModes
|
||||||
property.org.allowedTransportModes.isNotEmpty() -> property.org.allowedTransportModes
|
} else {
|
||||||
else -> TransportMode.entries.toSet()
|
TransportMode.entries.toSet()
|
||||||
}
|
}
|
||||||
return TransportMode.entries.map { mode ->
|
return TransportMode.entries.map { mode ->
|
||||||
TransportModeStatusResponse(
|
TransportModeStatusResponse(
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ data class GuestRatingCreateRequest(
|
|||||||
|
|
||||||
data class GuestRatingResponse(
|
data class GuestRatingResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val orgId: UUID,
|
|
||||||
val propertyId: UUID,
|
val propertyId: UUID,
|
||||||
val guestId: UUID,
|
val guestId: UUID,
|
||||||
val bookingId: UUID,
|
val bookingId: UUID,
|
||||||
|
|||||||
@@ -2,19 +2,6 @@ package com.android.trisolarisserver.controller.dto
|
|||||||
|
|
||||||
import java.util.UUID
|
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(
|
data class PropertyCreateRequest(
|
||||||
val code: String,
|
val code: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -41,7 +28,6 @@ data class PropertyUpdateRequest(
|
|||||||
|
|
||||||
data class PropertyResponse(
|
data class PropertyResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val orgId: UUID,
|
|
||||||
val code: String,
|
val code: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val addressText: String?,
|
val addressText: String?,
|
||||||
@@ -55,7 +41,6 @@ data class PropertyResponse(
|
|||||||
|
|
||||||
data class GuestResponse(
|
data class GuestResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val orgId: UUID,
|
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val phoneE164: String?,
|
val phoneE164: String?,
|
||||||
val nationality: String?,
|
val nationality: String?,
|
||||||
@@ -75,11 +60,11 @@ data class TransportModeStatusResponse(
|
|||||||
|
|
||||||
data class UserResponse(
|
data class UserResponse(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val orgId: UUID,
|
|
||||||
val firebaseUid: String?,
|
val firebaseUid: String?,
|
||||||
val phoneE164: String?,
|
val phoneE164: String?,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val disabled: Boolean
|
val disabled: Boolean,
|
||||||
|
val superAdmin: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
data class PropertyUserRoleRequest(
|
data class PropertyUserRoleRequest(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ data class RoomResponse(
|
|||||||
val id: UUID,
|
val id: UUID,
|
||||||
val roomNumber: Int,
|
val roomNumber: Int,
|
||||||
val floor: Int?,
|
val floor: Int?,
|
||||||
val roomTypeId: UUID,
|
|
||||||
val roomTypeName: String,
|
val roomTypeName: String,
|
||||||
val hasNfc: Boolean,
|
val hasNfc: Boolean,
|
||||||
val active: Boolean,
|
val active: Boolean,
|
||||||
@@ -52,7 +51,7 @@ enum class RoomBoardStatus {
|
|||||||
data class RoomUpsertRequest(
|
data class RoomUpsertRequest(
|
||||||
val roomNumber: Int,
|
val roomNumber: Int,
|
||||||
val floor: Int?,
|
val floor: Int?,
|
||||||
val roomTypeId: UUID,
|
val roomTypeCode: String,
|
||||||
val hasNfc: Boolean,
|
val hasNfc: Boolean,
|
||||||
val active: Boolean,
|
val active: Boolean,
|
||||||
val maintenance: Boolean,
|
val maintenance: Boolean,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.android.trisolarisserver.controller.dto
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class ActiveRoomStayResponse(
|
||||||
|
val roomStayId: UUID,
|
||||||
|
val bookingId: UUID,
|
||||||
|
val guestId: UUID?,
|
||||||
|
val guestName: String?,
|
||||||
|
val guestPhone: String?,
|
||||||
|
val roomId: UUID,
|
||||||
|
val roomNumber: Int,
|
||||||
|
val roomTypeName: String,
|
||||||
|
val fromAt: String,
|
||||||
|
val checkinAt: String?,
|
||||||
|
val expectedCheckoutAt: String?
|
||||||
|
)
|
||||||
@@ -7,7 +7,10 @@ data class RoomTypeUpsertRequest(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val baseOccupancy: Int? = null,
|
val baseOccupancy: Int? = null,
|
||||||
val maxOccupancy: Int? = null,
|
val maxOccupancy: Int? = null,
|
||||||
val otaAliases: Set<String>? = null
|
val sqFeet: Int? = null,
|
||||||
|
val bathroomSqFeet: Int? = null,
|
||||||
|
val otaAliases: Set<String>? = null,
|
||||||
|
val amenityIds: Set<UUID>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RoomTypeResponse(
|
data class RoomTypeResponse(
|
||||||
@@ -17,5 +20,21 @@ data class RoomTypeResponse(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val baseOccupancy: Int,
|
val baseOccupancy: Int,
|
||||||
val maxOccupancy: Int,
|
val maxOccupancy: Int,
|
||||||
val otaAliases: Set<String>
|
val sqFeet: Int?,
|
||||||
|
val bathroomSqFeet: Int?,
|
||||||
|
val otaAliases: Set<String>,
|
||||||
|
val amenities: Set<AmenityResponse>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AmenityUpsertRequest(
|
||||||
|
val name: String,
|
||||||
|
val category: String? = null,
|
||||||
|
val iconKey: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AmenityResponse(
|
||||||
|
val id: UUID,
|
||||||
|
val name: String,
|
||||||
|
val category: String?,
|
||||||
|
val iconKey: String?
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface GuestRepo : JpaRepository<Guest, 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
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
import com.android.trisolarisserver.models.property.Organization
|
import com.android.trisolarisserver.models.property.Property
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "guest",
|
name = "guest",
|
||||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "phone_e164"])]
|
uniqueConstraints = [UniqueConstraint(columnNames = ["property_id", "phone_e164"])]
|
||||||
)
|
)
|
||||||
class Guest(
|
class Guest(
|
||||||
@Id
|
@Id
|
||||||
@@ -17,8 +17,8 @@ class Guest(
|
|||||||
val id: UUID? = null,
|
val id: UUID? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
@JoinColumn(name = "org_id", nullable = false)
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
var org: Organization,
|
var property: Property,
|
||||||
|
|
||||||
@Column(name = "phone_e164")
|
@Column(name = "phone_e164")
|
||||||
var phoneE164: String? = null,
|
var phoneE164: String? = null,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.android.trisolarisserver.models.booking
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
import com.android.trisolarisserver.models.property.AppUser
|
import com.android.trisolarisserver.models.property.AppUser
|
||||||
import com.android.trisolarisserver.models.property.Organization
|
|
||||||
import com.android.trisolarisserver.models.property.Property
|
import com.android.trisolarisserver.models.property.Property
|
||||||
import jakarta.persistence.Column
|
import jakarta.persistence.Column
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.Entity
|
||||||
@@ -30,10 +29,6 @@ class GuestRating(
|
|||||||
@Column(columnDefinition = "uuid")
|
@Column(columnDefinition = "uuid")
|
||||||
val id: UUID? = null,
|
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)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
@JoinColumn(name = "property_id", nullable = false)
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
var property: Property,
|
var property: Property,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.android.trisolarisserver.models.booking
|
package com.android.trisolarisserver.models.booking
|
||||||
|
|
||||||
import com.android.trisolarisserver.models.property.Organization
|
import com.android.trisolarisserver.models.property.Property
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -8,7 +8,7 @@ import java.util.UUID
|
|||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "guest_vehicle",
|
name = "guest_vehicle",
|
||||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "vehicle_number"])]
|
uniqueConstraints = [UniqueConstraint(columnNames = ["property_id", "vehicle_number"])]
|
||||||
)
|
)
|
||||||
class GuestVehicle(
|
class GuestVehicle(
|
||||||
@Id
|
@Id
|
||||||
@@ -17,8 +17,8 @@ class GuestVehicle(
|
|||||||
val id: UUID? = null,
|
val id: UUID? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
@JoinColumn(name = "org_id", nullable = false)
|
@JoinColumn(name = "property_id", nullable = false)
|
||||||
var org: Organization,
|
var property: Property,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
@JoinColumn(name = "guest_id", nullable = false)
|
@JoinColumn(name = "guest_id", nullable = false)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package com.android.trisolarisserver.models.property
|
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.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -15,10 +20,6 @@ class AppUser(
|
|||||||
@Column(columnDefinition = "uuid")
|
@Column(columnDefinition = "uuid")
|
||||||
val id: UUID? = null,
|
val id: UUID? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@JoinColumn(name = "org_id", nullable = false)
|
|
||||||
var org: Organization,
|
|
||||||
|
|
||||||
@Column(name = "firebase_uid")
|
@Column(name = "firebase_uid")
|
||||||
var firebaseUid: String? = null, // optional if using firebase
|
var firebaseUid: String? = null, // optional if using firebase
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ class AppUser(
|
|||||||
|
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "is_super_admin", nullable = false)
|
||||||
|
var superAdmin: Boolean = false,
|
||||||
|
|
||||||
@Column(name = "is_disabled", nullable = false)
|
@Column(name = "is_disabled", nullable = false)
|
||||||
var disabled: Boolean = 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,13 +1,24 @@
|
|||||||
package com.android.trisolarisserver.models.property
|
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.time.OffsetDateTime
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(
|
@Table(
|
||||||
name = "property",
|
name = "property",
|
||||||
uniqueConstraints = [UniqueConstraint(columnNames = ["org_id", "code"])]
|
uniqueConstraints = [UniqueConstraint(columnNames = ["code"])]
|
||||||
)
|
)
|
||||||
class Property(
|
class Property(
|
||||||
@Id
|
@Id
|
||||||
@@ -15,10 +26,6 @@ class Property(
|
|||||||
@Column(columnDefinition = "uuid")
|
@Column(columnDefinition = "uuid")
|
||||||
val id: UUID? = null,
|
val id: UUID? = null,
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@JoinColumn(name = "org_id", nullable = false)
|
|
||||||
var org: Organization,
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
var code: String, // "TRI-VNS"
|
var code: String, // "TRI-VNS"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.android.trisolarisserver.models.room
|
||||||
|
|
||||||
|
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 = "room_amenity",
|
||||||
|
uniqueConstraints = [UniqueConstraint(columnNames = ["name"])]
|
||||||
|
)
|
||||||
|
class RoomAmenity(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(columnDefinition = "uuid")
|
||||||
|
val id: UUID? = null,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@Column
|
||||||
|
var category: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "icon_key")
|
||||||
|
var iconKey: String? = null,
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
|
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
|
)
|
||||||
@@ -32,6 +32,12 @@ class RoomType(
|
|||||||
@Column(name = "max_occupancy", nullable = false)
|
@Column(name = "max_occupancy", nullable = false)
|
||||||
var maxOccupancy: Int = 3,
|
var maxOccupancy: Int = 3,
|
||||||
|
|
||||||
|
@Column(name = "sq_feet")
|
||||||
|
var sqFeet: Int? = null,
|
||||||
|
|
||||||
|
@Column(name = "bathroom_sq_feet")
|
||||||
|
var bathroomSqFeet: Int? = null,
|
||||||
|
|
||||||
@ElementCollection(fetch = FetchType.EAGER)
|
@ElementCollection(fetch = FetchType.EAGER)
|
||||||
@CollectionTable(
|
@CollectionTable(
|
||||||
name = "room_type_alias",
|
name = "room_type_alias",
|
||||||
@@ -40,6 +46,14 @@ class RoomType(
|
|||||||
@Column(name = "alias", nullable = false)
|
@Column(name = "alias", nullable = false)
|
||||||
var otaAliases: MutableSet<String> = mutableSetOf(),
|
var otaAliases: MutableSet<String> = mutableSetOf(),
|
||||||
|
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(
|
||||||
|
name = "room_type_amenity_link",
|
||||||
|
joinColumns = [JoinColumn(name = "room_type_id")],
|
||||||
|
inverseJoinColumns = [JoinColumn(name = "amenity_id")]
|
||||||
|
)
|
||||||
|
var amenities: MutableSet<RoomAmenity> = mutableSetOf(),
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
@Column(name = "created_at", nullable = false, columnDefinition = "timestamptz")
|
||||||
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
val createdAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ import java.util.UUID
|
|||||||
interface AppUserRepo : JpaRepository<AppUser, UUID> {
|
interface AppUserRepo : JpaRepository<AppUser, UUID> {
|
||||||
fun findByFirebaseUid(firebaseUid: String): AppUser?
|
fun findByFirebaseUid(firebaseUid: String): AppUser?
|
||||||
fun existsByFirebaseUid(firebaseUid: String): Boolean
|
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
|
import java.util.UUID
|
||||||
|
|
||||||
interface GuestVehicleRepo : JpaRepository<GuestVehicle, 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 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
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,6 @@ import org.springframework.data.jpa.repository.JpaRepository
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface PropertyRepo : JpaRepository<Property, UUID> {
|
interface PropertyRepo : JpaRepository<Property, UUID> {
|
||||||
fun existsByOrgIdAndCode(orgId: UUID, code: String): Boolean
|
fun existsByCode(code: String): Boolean
|
||||||
fun existsByOrgIdAndCodeAndIdNot(orgId: UUID, code: String, id: UUID): Boolean
|
fun existsByCodeAndIdNot(code: String, id: UUID): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,6 @@ interface PropertyUserRepo : JpaRepository<PropertyUser, PropertyUserId> {
|
|||||||
@Param("userId") userId: UUID
|
@Param("userId") userId: UUID
|
||||||
): Set<Role>
|
): 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("""
|
@Query("""
|
||||||
select case when count(pu) > 0 then true else false end
|
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>
|
@Param("roles") roles: Set<Role>
|
||||||
): Boolean
|
): 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.android.trisolarisserver.repo
|
||||||
|
|
||||||
|
import com.android.trisolarisserver.models.room.RoomAmenity
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface RoomAmenityRepo : JpaRepository<RoomAmenity, UUID> {
|
||||||
|
fun findAllByOrderByName(): List<RoomAmenity>
|
||||||
|
fun findByIdIn(ids: Set<UUID>): List<RoomAmenity>
|
||||||
|
fun existsByName(name: String): Boolean
|
||||||
|
fun existsByNameAndIdNot(name: String, id: UUID): Boolean
|
||||||
|
}
|
||||||
@@ -10,9 +10,10 @@ import java.util.UUID
|
|||||||
|
|
||||||
interface RoomRepo : JpaRepository<Room, UUID> {
|
interface RoomRepo : JpaRepository<Room, UUID> {
|
||||||
|
|
||||||
@EntityGraph(attributePaths = ["roomType"])
|
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"])
|
||||||
fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List<Room>
|
fun findByPropertyIdOrderByRoomNumber(propertyId: UUID): List<Room>
|
||||||
|
|
||||||
|
@EntityGraph(attributePaths = ["roomType", "roomType.otaAliases", "roomType.amenities"])
|
||||||
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): Room?
|
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): Room?
|
||||||
|
|
||||||
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean
|
fun existsByPropertyIdAndRoomNumber(propertyId: UUID, roomNumber: Int): Boolean
|
||||||
|
|||||||
@@ -62,4 +62,17 @@ interface RoomStayRepo : JpaRepository<RoomStay, UUID> {
|
|||||||
@Param("fromAt") fromAt: java.time.OffsetDateTime,
|
@Param("fromAt") fromAt: java.time.OffsetDateTime,
|
||||||
@Param("toAt") toAt: java.time.OffsetDateTime
|
@Param("toAt") toAt: java.time.OffsetDateTime
|
||||||
): Boolean
|
): Boolean
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select rs
|
||||||
|
from RoomStay rs
|
||||||
|
join fetch rs.room r
|
||||||
|
join fetch r.roomType rt
|
||||||
|
join fetch rs.booking b
|
||||||
|
left join fetch b.primaryGuest g
|
||||||
|
where rs.property.id = :propertyId
|
||||||
|
and rs.toAt is null
|
||||||
|
order by r.roomNumber
|
||||||
|
""")
|
||||||
|
fun findActiveByPropertyIdWithDetails(@Param("propertyId") propertyId: UUID): List<RoomStay>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import org.springframework.data.jpa.repository.JpaRepository
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
|
interface RoomTypeRepo : JpaRepository<RoomType, UUID> {
|
||||||
|
@EntityGraph(attributePaths = ["property", "otaAliases", "amenities"])
|
||||||
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
|
fun findByIdAndPropertyId(id: UUID, propertyId: UUID): RoomType?
|
||||||
@EntityGraph(attributePaths = ["property"])
|
fun findByPropertyIdAndCodeIgnoreCase(propertyId: UUID, code: String): RoomType?
|
||||||
|
@EntityGraph(attributePaths = ["property", "otaAliases", "amenities"])
|
||||||
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
|
fun findByPropertyIdOrderByCode(propertyId: UUID): List<RoomType>
|
||||||
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
|
fun existsByPropertyIdAndCode(propertyId: UUID, code: String): Boolean
|
||||||
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean
|
fun existsByPropertyIdAndCodeAndIdNot(propertyId: UUID, code: String, id: UUID): Boolean
|
||||||
|
fun existsByAmenitiesId(id: UUID): Boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.google.firebase.auth.FirebaseAuth
|
|||||||
import jakarta.servlet.FilterChain
|
import jakarta.servlet.FilterChain
|
||||||
import jakarta.servlet.http.HttpServletRequest
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
import jakarta.servlet.http.HttpServletResponse
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
@@ -17,10 +18,11 @@ import org.springframework.http.HttpStatus
|
|||||||
class FirebaseAuthFilter(
|
class FirebaseAuthFilter(
|
||||||
private val appUserRepo: AppUserRepo
|
private val appUserRepo: AppUserRepo
|
||||||
) : OncePerRequestFilter() {
|
) : OncePerRequestFilter() {
|
||||||
|
private val logger = LoggerFactory.getLogger(FirebaseAuthFilter::class.java)
|
||||||
|
|
||||||
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
|
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
|
||||||
val path = request.requestURI
|
val path = request.requestURI
|
||||||
return path == "/" || path == "/health"
|
return path == "/" || path == "/health" || path.startsWith("/auth/")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun doFilterInternal(
|
override fun doFilterInternal(
|
||||||
@@ -30,6 +32,7 @@ class FirebaseAuthFilter(
|
|||||||
) {
|
) {
|
||||||
val header = request.getHeader(HttpHeaders.AUTHORIZATION)
|
val header = request.getHeader(HttpHeaders.AUTHORIZATION)
|
||||||
if (header.isNullOrBlank() || !header.startsWith("Bearer ")) {
|
if (header.isNullOrBlank() || !header.startsWith("Bearer ")) {
|
||||||
|
logger.debug("Auth missing/invalid header for {}", request.requestURI)
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing Authorization token")
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing Authorization token")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -39,6 +42,7 @@ class FirebaseAuthFilter(
|
|||||||
val firebaseUid = decoded.uid
|
val firebaseUid = decoded.uid
|
||||||
val user = appUserRepo.findByFirebaseUid(firebaseUid)
|
val user = appUserRepo.findByFirebaseUid(firebaseUid)
|
||||||
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User not found")
|
||||||
|
logger.debug("Auth verified uid={}, userId={}", firebaseUid, user.id)
|
||||||
|
|
||||||
val principal = MyPrincipal(
|
val principal = MyPrincipal(
|
||||||
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
|
userId = user.id ?: throw ResponseStatusException(HttpStatus.UNAUTHORIZED, "User id missing"),
|
||||||
@@ -48,6 +52,7 @@ class FirebaseAuthFilter(
|
|||||||
SecurityContextHolder.getContext().authentication = auth
|
SecurityContextHolder.getContext().authentication = auth
|
||||||
filterChain.doFilter(request, response)
|
filterChain.doFilter(request, response)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
logger.debug("Auth failed for {}: {}", request.requestURI, ex.message)
|
||||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token")
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ package com.android.trisolarisserver.security
|
|||||||
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy
|
import org.springframework.security.config.http.SessionCreationPolicy
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||||
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
|
||||||
@Configuration
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableMethodSecurity
|
||||||
class SecurityConfig(
|
class SecurityConfig(
|
||||||
private val firebaseAuthFilter: FirebaseAuthFilter
|
private val firebaseAuthFilter: FirebaseAuthFilter,
|
||||||
|
private val objectMapper: ObjectMapper
|
||||||
) {
|
) {
|
||||||
@Bean
|
@Bean
|
||||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
@@ -17,10 +25,38 @@ class SecurityConfig(
|
|||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers("/", "/health").permitAll()
|
it.requestMatchers("/", "/health", "/auth/**").permitAll()
|
||||||
it.anyRequest().authenticated()
|
it.anyRequest().authenticated()
|
||||||
}
|
}
|
||||||
|
.exceptionHandling {
|
||||||
|
it.authenticationEntryPoint { request, response, _ ->
|
||||||
|
writeError(response, request, HttpStatus.UNAUTHORIZED, "Unauthorized")
|
||||||
|
}
|
||||||
|
it.accessDeniedHandler { request, response, _ ->
|
||||||
|
writeError(response, request, HttpStatus.FORBIDDEN, "Forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.httpBasic { it.disable() }
|
||||||
|
.formLogin { it.disable() }
|
||||||
.addFilterBefore(firebaseAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
|
.addFilterBefore(firebaseAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun writeError(
|
||||||
|
response: HttpServletResponse,
|
||||||
|
request: HttpServletRequest,
|
||||||
|
status: HttpStatus,
|
||||||
|
message: String
|
||||||
|
) {
|
||||||
|
if (response.isCommitted) return
|
||||||
|
response.status = status.value()
|
||||||
|
response.contentType = "application/json"
|
||||||
|
val body = mapOf(
|
||||||
|
"status" to status.value(),
|
||||||
|
"error" to status.reasonPhrase,
|
||||||
|
"message" to message,
|
||||||
|
"path" to request.requestURI
|
||||||
|
)
|
||||||
|
response.writer.use { it.write(objectMapper.writeValueAsString(body)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,11 +157,11 @@ class EmailIngestionService(
|
|||||||
private fun resolveGuest(property: Property, extracted: Map<String, String>): Guest {
|
private fun resolveGuest(property: Property, extracted: Map<String, String>): Guest {
|
||||||
val phone = extracted["guestPhone"]?.takeIf { !it.contains("NONE", true) }?.trim()
|
val phone = extracted["guestPhone"]?.takeIf { !it.contains("NONE", true) }?.trim()
|
||||||
if (!phone.isNullOrBlank()) {
|
if (!phone.isNullOrBlank()) {
|
||||||
val existing = guestRepo.findByOrgIdAndPhoneE164(property.org.id!!, phone)
|
val existing = guestRepo.findByPropertyIdAndPhoneE164(property.id!!, phone)
|
||||||
if (existing != null) return existing
|
if (existing != null) return existing
|
||||||
}
|
}
|
||||||
val guest = Guest(
|
val guest = Guest(
|
||||||
org = property.org,
|
property = property,
|
||||||
phoneE164 = phone,
|
phoneE164 = phone,
|
||||||
name = extracted["guestName"]?.takeIf { !it.contains("NONE", true) }
|
name = extracted["guestName"]?.takeIf { !it.contains("NONE", true) }
|
||||||
)
|
)
|
||||||
@@ -281,10 +281,6 @@ class EmailIngestionService(
|
|||||||
if (propertyEmails.isNotEmpty() && recipients.any { it.lowercase() in propertyEmails }) {
|
if (propertyEmails.isNotEmpty() && recipients.any { it.lowercase() in propertyEmails }) {
|
||||||
return@filter true
|
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>()
|
val aliases = mutableSetOf<String>()
|
||||||
aliases.add(property.name)
|
aliases.add(property.name)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/trisolaris
|
spring.datasource.url=jdbc:postgresql://localhost:5432/trisolaris
|
||||||
ai.llama.baseUrl=http://localhost:8089/v1/chat/completions
|
ai.llama.baseUrl=http://localhost:8089/v1/chat/completions
|
||||||
|
logging.level.com.android.trisolarisserver.controller.Auth=INFO
|
||||||
|
|||||||
Reference in New Issue
Block a user