From 730a1d782fc65e4b629a57a3554601b1231dc8f7 Mon Sep 17 00:00:00 2001 From: androidlover5842 Date: Wed, 4 Feb 2026 12:30:45 +0530 Subject: [PATCH] Remove generated API reference tooling and keep manual catalog --- docs/API_CATALOG.md | 1764 ++++------------------------------ docs/API_REFERENCE.md | 144 --- scripts/generate_api_docs.py | 888 ----------------- 3 files changed, 193 insertions(+), 2603 deletions(-) delete mode 100644 docs/API_REFERENCE.md delete mode 100644 scripts/generate_api_docs.py diff --git a/docs/API_CATALOG.md b/docs/API_CATALOG.md index d81a07e..c34cee6 100644 --- a/docs/API_CATALOG.md +++ b/docs/API_CATALOG.md @@ -1,1575 +1,197 @@ # API Catalog -Behavior-first catalog generated from controller source. +Manual source of truth for implemented HTTP endpoints. - Total endpoints: **125** -- Notes: validations/errors are extracted from explicit `ResponseStatusException` checks and shared helper guards. -- Regenerate: `python scripts/generate_api_docs.py` - -## `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt` - -### `GET /icons/png` - -- Handler: `listPng` (`src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:23`) -- Behavior: List resources (list png). -- Body: none -- Auth: Public/unspecified -- Response: `200` `List` -- Common errors: none observed in controller checks - -### `GET /icons/png/{filename}` - -- Handler: `getPng` (`src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:39`) -- Behavior: Get resource (get png). -- Path params: filename:String -- Body: none -- Auth: Public/unspecified -- Response: `200` `ResponseEntity` -- Common errors: none observed in controller checks - -## `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt` - -### `GET /auth/me` - -- Handler: `me` (`src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:44`) -- Behavior: Me. -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `ResponseEntity` -- Common errors: none observed in controller checks - -### `PUT /auth/me` - -- Handler: `updateMe` (`src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:54`) -- Behavior: Update resource (update me). -- Body: UpdateMeRequest { name:String? (optional) } -- Auth: Authenticated user (Firebase) -- Response: `200` `ResponseEntity` -- Common errors: 401 (User not found) - -### `POST /auth/verify` - -- Handler: `verify` (`src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:33`) -- Behavior: Verify. -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `ResponseEntity` -- Common errors: none observed in controller checks - -## `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingBalances.kt` - -### `GET /properties/{propertyId}/bookings/{bookingId}/balance` - -- Handler: `getBalance` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingBalances.kt:32`) -- Behavior: Get resource (get balance). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `BookingBalanceResponse` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) - -## `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt` - -### `GET /properties/{propertyId}/bookings` - -- Handler: `listBookings` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:187`) -- Behavior: List resources (list bookings). -- Path params: propertyId:UUID -- Query params: status:String? (optional) -- Body: none -- Auth: Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `List` -- Validation/guard checks: - - 400: Invalid status: $value -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (Invalid status: $value) - -### `POST /properties/{propertyId}/bookings` - -- Handler: `createBooking` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:99`) -- Behavior: Create resource (create booking). -- Path params: propertyId:UUID -- Body: BookingCreateRequest { source:String? (optional), expectedCheckInAt:String, expectedCheckOutAt:String, billingMode:String? (optional), billingCheckoutTime:String? (optional), guestPhoneE164:String? (optional), fromCity:String? (optional), toCity:String? (optional), memberRelation:String? (optional), transportMode:String? (optional), childCount:Int? (optional), maleCount:Int? (optional), femaleCount:Int? (optional), expectedGuestCount:Int? (optional), notes:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `201` `BookingCreateResponse` -- Validation/guard checks: - - 400: expectedCheckInAt required - - 400: expectedCheckOutAt required - - 400: Invalid date range - - 400: Transport mode disabled -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (expectedCheckInAt required; expectedCheckOutAt required) - -### `GET /properties/{propertyId}/bookings/{bookingId}` - -- Handler: `getBooking` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:282`) -- Behavior: Get resource (get booking). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `BookingDetailResponse` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/billing-policy` - -- Handler: `updateBillingPolicy` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:476`) -- Behavior: Update resource (update billing policy). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingBillingPolicyUpdateRequest { billingMode:String, billingCheckoutTime:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 400: $fieldName required - - 400: $fieldName must be HH:mm - - 400: Unknown billing mode - - 409: Booking closed -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 ($fieldName required; $fieldName must be HH:mm), 409 (Booking closed) - -### `POST /properties/{propertyId}/bookings/{bookingId}/cancel` - -- Handler: `cancel` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:695`) -- Behavior: Cancel flow (cancel). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingCancelRequest { cancelledAt:String? (optional), reason:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 409: Cannot cancel checked-in booking -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Cannot cancel checked-in booking) - -### `POST /properties/{propertyId}/bookings/{bookingId}/check-in/bulk` - -- Handler: `bulkCheckIn` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:353`) -- Behavior: Bulk check in. -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingBulkCheckInRequest { stays:List, transportMode:String? (optional), notes:String? (optional) } -- Side effects: Emits booking SSE updates. Emits room board SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `201` `Unit` -- Validation/guard checks: - - 400: stays required - - 400: Duplicate roomId in stays - - 400: Transport mode disabled - - 400: Unknown transport mode - - 409: Booking not open - - 409: Room not available - - 409: Room already occupied -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Room not found; Booking not found), 400 (stays required; Duplicate roomId in stays), 409 (Booking not open; Room not available) - -### `POST /properties/{propertyId}/bookings/{bookingId}/check-out` - -- Handler: `checkOut` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:527`) -- Behavior: Check out flow (check out). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingCheckOutRequest { checkOutAt:String? (optional), notes:String? (optional) } -- Side effects: Emits booking SSE updates. Emits room board SSE updates. Writes room-stay audit log. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Invalid timestamp - - 409: Booking not checked in - - 409: Room stay amount is outside allowed range - - 409: Ledger mismatch: collected amount must be within 20% of expected amount before checkout -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Invalid timestamp), 409 (Booking not checked in; Room stay amount is outside allowed range) - -### `POST /properties/{propertyId}/bookings/{bookingId}/expected-dates` - -- Handler: `updateExpectedDates` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:430`) -- Behavior: Update resource (update expected dates). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingExpectedDatesUpdateRequest { expectedCheckInAt:String? (optional), expectedCheckOutAt:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Invalid date range - - 400: Invalid timestamp - - 409: Cannot change expected check-in after check-in - - 409: Booking closed -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Invalid date range; Invalid timestamp), 409 (Cannot change expected check-in after check-in; Booking closed) - -### `POST /properties/{propertyId}/bookings/{bookingId}/link-guest` - -- Handler: `linkGuest` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:326`) -- Behavior: Link guest. -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingLinkGuestRequest { guestId:UUID } -- Side effects: Emits booking SSE updates. -- Auth: Any property member -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Guest not in property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Guest not found; Booking not found), 400 (Guest not in property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/no-show` - -- Handler: `noShow` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:720`) -- Behavior: No-show flow (no show). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingNoShowRequest { noShowAt:String? (optional), reason:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 409: Booking not open -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Booking not open) - -### `POST /properties/{propertyId}/bookings/{bookingId}/room-stays/{roomStayId}/check-out` - -- Handler: `checkOutRoomStay` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:583`) -- Behavior: Check out flow (check out room stay). -- Path params: propertyId:UUID, bookingId:UUID, roomStayId:UUID -- Body: BookingCheckOutRequest { checkOutAt:String? (optional), notes:String? (optional) } -- Side effects: Emits booking SSE updates. Emits room board SSE updates. Writes room-stay audit log. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Invalid timestamp - - 409: Booking not checked in - - 409: Room stay amount is outside allowed range - - 409: Minimum stay duration is 1 hour - - 409: Ledger mismatch: collected amount must be within 20% of expected amount before checkout -- Common errors: 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Room stay not found for booking; Booking not found), 400 (Invalid timestamp), 409 (Booking not checked in; Room stay amount is outside allowed range) - -### `GET /properties/{propertyId}/bookings/{bookingId}/stream` - -- Handler: `streamBooking` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:302`) -- Behavior: Stream events/data (stream booking). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Side effects: Streams SSE events. -- Auth: Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -## `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt` - -### `GET /properties/{propertyId}/bookings/{bookingId}/room-requests` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:103`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/room-requests` - -- Handler: `create` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:48`) -- Behavior: Create resource (create). -- Path params: propertyId:UUID, bookingId:UUID -- Body: BookingRoomRequestCreateRequest { roomTypeCode:String, quantity:Int, fromAt:String, toAt:String } -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `201` `BookingRoomRequestResponse` -- Validation/guard checks: - - 400: quantity must be > 0 - - 400: fromAt required - - 400: toAt required - - 400: Invalid date range - - 409: Booking closed - - 409: Insufficient room type availability -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (quantity must be > 0; fromAt required), 409 (Booking closed; Insufficient room type availability) - -### `DELETE /properties/{propertyId}/bookings/{bookingId}/room-requests/{requestId}` - -- Handler: `cancel` (`src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:121`) -- Behavior: Cancel flow (cancel). -- Path params: propertyId:UUID, bookingId:UUID, requestId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `204` `Unit` -- Validation/guard checks: - - 409: Cannot cancel fulfilled room request -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Cannot cancel fulfilled room request) - -## `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt` - -### `GET /properties/{propertyId}/room-stays/cards/{cardIndex}` - -- Handler: `getCardByIndex` (`src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:158`) -- Behavior: Get resource (get card by index). -- Path params: propertyId:UUID, cardIndex:Int -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `IssuedCardResponse` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Card not found) - -### `POST /properties/{propertyId}/room-stays/cards/{cardIndex}/revoke` - -- Handler: `revoke` (`src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:138`) -- Behavior: Revoke. -- Path params: propertyId:UUID, cardIndex:Int -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `CardRevokeResponse` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Card not found) - -### `GET /properties/{propertyId}/room-stays/{roomStayId}/cards` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:125`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, roomStayId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF, SUPERVISOR -- Response: `200` `List` -- Common errors: 401 (Missing principal), 404 (Room stay not found for property) - -### `POST /properties/{propertyId}/room-stays/{roomStayId}/cards` - -- Handler: `issue` (`src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:83`) -- Behavior: Issue. -- Path params: propertyId:UUID, roomStayId:UUID -- Body: IssueCardRequest { cardId:String, cardIndex:Int, issuedAt:String? (optional), expiresAt:String } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `IssuedCardResponse` -- Validation/guard checks: - - 400: cardId required - - 400: cardIndex required - - 400: expiresAt required - - 400: expiresAt must be after issuedAt - - 409: Active card already exists for room stay - - 409: Active card already exists for room - - 409: Room stay is already closed -- Common errors: 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room stay not found for property), 400 (cardId required; cardIndex required), 409 (Active card already exists for room stay; Active card already exists for room) - -### `POST /properties/{propertyId}/room-stays/{roomStayId}/cards/prepare` - -- Handler: `prepare` (`src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:50`) -- Behavior: Prepare. -- Path params: propertyId:UUID, roomStayId:UUID -- Body: CardPrepareRequest { expiresAt:String? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `CardPrepareResponse` -- Validation/guard checks: - - 400: expiresAt required - - 400: expiresAt must be after issuedAt - - 400: Invalid timestamp - - 409: Room stay is already closed -- Common errors: 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room stay not found for property; Property not found), 400 (expiresAt required; expiresAt must be after issuedAt), 409 (Room stay is already closed) - -## `src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt` - -### `POST /properties/{propertyId}/rooms/{roomId}/cards/prepare-temp` - -- Handler: `prepareTemporary` (`src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:46`) -- Behavior: Prepare temporary. -- Path params: propertyId:UUID, roomId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `CardPrepareResponse` -- Common errors: 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room not found; Property not found) - -### `POST /properties/{propertyId}/rooms/{roomId}/cards/temp` - -- Handler: `issueTemporary` (`src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:74`) -- Behavior: Issue temporary. -- Path params: propertyId:UUID, roomId:UUID -- Body: IssueTempCardRequest { cardId:String, cardIndex:Int, issuedAt:String? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `IssuedCardResponse` -- Validation/guard checks: - - 400: cardId required - - 400: cardIndex required - - 400: Invalid timestamp - - 409: Active card already exists for room -- Common errors: 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room not found), 400 (cardId required; cardIndex required), 409 (Active card already exists for room) - -## `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmailManual.kt` - -### `POST /properties/{propertyId}/inbound-emails/manual` - -- Handler: `uploadManualPdf` (`src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmailManual.kt:41`) -- Behavior: Upload manual pdf. -- Path params: propertyId:UUID -- Query params: file:MultipartFile (required) -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `ManualInboundResponse` -- Validation/guard checks: - - 400: File is empty - - 400: Only PDF is supported -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (File is empty; Only PDF is supported) - -## `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmails.kt` - -### `GET /properties/{propertyId}/inbound-emails/{emailId}/file` - -- Handler: `downloadEmailPdf` (`src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmails.kt:32`) -- Behavior: Download email pdf. -- Path params: propertyId:UUID, emailId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `ResponseEntity` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Email not found; Email PDF missing) - -## `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt` - -### `GET /properties/{propertyId}/guests/{guestId}/documents` - -- Handler: `listDocuments` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:125`) -- Behavior: List resources (list documents). -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Required property role not granted) - -### `POST /properties/{propertyId}/guests/{guestId}/documents` - -- Handler: `uploadDocument` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:65`) -- Behavior: Upload document. -- Path params: propertyId:UUID, guestId:UUID -- Query params: bookingId:UUID (required) -- Body: none -- Side effects: Stores/updates guest document metadata. -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `GuestDocumentResponse` -- Validation/guard checks: - - 400: File is empty - - 400: Video files are not allowed - - 400: Booking not in property - - 400: Booking not linked to guest - - 409: Duplicate document -- Common errors: 401 (Missing principal; User not found), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (File is empty; Video files are not allowed), 409 (Duplicate document) - -### `GET /properties/{propertyId}/guests/{guestId}/documents/stream` - -- Handler: `streamDocuments` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:138`) -- Behavior: Stream events/data (stream documents). -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Side effects: Streams SSE events. -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` -- Common errors: 401 (Missing principal), 403 (Required property role not granted) - -### `DELETE /properties/{propertyId}/guests/{guestId}/documents/{documentId}` - -- Handler: `deleteDocument` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:184`) -- Behavior: Delete resource (delete document). -- Path params: propertyId:UUID, guestId:UUID, documentId:UUID -- Body: none -- Side effects: Deletes guest document metadata. -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Documents can only be deleted for OPEN or CHECKED_IN bookings -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Document not found), 400 (Documents can only be deleted for OPEN or CHECKED_IN bookings), 500 (Failed to delete file) - -### `GET /properties/{propertyId}/guests/{guestId}/documents/{documentId}/file` - -- Handler: `downloadDocument` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:152`) -- Behavior: Download document. -- Path params: propertyId:UUID, guestId:UUID, documentId:UUID -- Query params: token:String? (optional) -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `ResponseEntity` -- Common errors: 401 (Invalid token; Missing principal), 403 (Required property role not granted), 404 (Document not found; File missing) - -## `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt` - -### `GET /properties/{propertyId}/guests/{guestId}/ratings` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:79`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Validation/guard checks: - - 400: Guest not in property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property) - -### `POST /properties/{propertyId}/guests/{guestId}/ratings` - -- Handler: `create` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:42`) -- Behavior: Create resource (create). -- Path params: propertyId:UUID, guestId:UUID -- Body: GuestRatingCreateRequest { bookingId:UUID, score:String, notes:String? (optional) } -- Auth: Any property member -- Response: `201` `GuestRatingResponse` -- Validation/guard checks: - - 400: Booking not in property - - 400: Booking not linked to guest - - 400: score must be GOOD/OK/TROUBLE or 1/2/3 - - 400: Guest not in property - - 409: Rating already exists for booking -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (Booking not in property; Booking not linked to guest), 409 (Rating already exists for booking) - -## `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt` - -### `GET /properties/{propertyId}/guests/search` - -- Handler: `search` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:75`) -- Behavior: Search. -- Path params: propertyId:UUID -- Query params: phone:String? (optional), vehicleNumber:String? (optional) -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Validation/guard checks: - - 400: phone or vehicleNumber required -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (phone or vehicleNumber required) - -### `GET /properties/{propertyId}/guests/visit-count` - -- Handler: `getVisitCount` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:113`) -- Behavior: Get resource (get visit count). -- Path params: propertyId:UUID -- Query params: phone:String (required) -- Body: none -- Auth: Any property member -- Response: `200` `GuestVisitCountResponse` -- Validation/guard checks: - - 400: phone required -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (phone required) - -### `GET /properties/{propertyId}/guests/{guestId}` - -- Handler: `getGuest` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:102`) -- Behavior: Get resource (get guest). -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `GuestResponse` -- Validation/guard checks: - - 400: Guest not in property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property) - -### `PUT /properties/{propertyId}/guests/{guestId}` - -- Handler: `updateGuest` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:45`) -- Behavior: Update resource (update guest). -- Path params: propertyId:UUID, guestId:UUID -- Body: GuestUpdateRequest { phoneE164:String? (optional), name:String? (optional), nationality:String? (optional), addressText:String? (optional) } -- Auth: Any property member -- Response: `200` `GuestResponse` -- Validation/guard checks: - - 400: Guest not in property - - 409: Phone number already exists -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property), 409 (Phone number already exists) - -### `POST /properties/{propertyId}/guests/{guestId}/signature` - -- Handler: `uploadSignature` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:170`) -- Behavior: Upload signature. -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `GuestResponse` -- Validation/guard checks: - - 400: File is empty - - 400: Only SVG allowed - - 400: Guest not in property -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property or guest not found), 400 (File is empty; Only SVG allowed) - -### `GET /properties/{propertyId}/guests/{guestId}/signature/file` - -- Handler: `downloadSignature` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:194`) -- Behavior: Download signature. -- Path params: propertyId:UUID, guestId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `ResponseEntity` -- Validation/guard checks: - - 400: Guest not in property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Signature not found; Property or guest not found), 400 (Guest not in property) - -### `POST /properties/{propertyId}/guests/{guestId}/vehicles` - -- Handler: `addVehicle` (`src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:130`) -- Behavior: Add vehicle. -- Path params: propertyId:UUID, guestId:UUID -- Body: GuestVehicleRequest { vehicleNumber:String, bookingId:UUID } -- Auth: Any property member -- Response: `201` `GuestResponse` -- Validation/guard checks: - - 400: Booking not in property - - 400: Guest not in property - - 409: Booking linked to different guest - - 409: Vehicle number already exists -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (Booking not in property; Guest not in property), 409 (Booking linked to different guest; Vehicle number already exists) - -## `src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt` - -### `GET /properties/{propertyId}/bookings/{bookingId}/charges` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:80`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/charges` - -- Handler: `create` (`src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:44`) -- Behavior: Create resource (create). -- Path params: propertyId:UUID, bookingId:UUID -- Body: ChargeCreateRequest { type:String, amount:Long, currency:String, occurredAt:String? (optional), notes:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, FINANCE, MANAGER -- Response: `201` `ChargeResponse` -- Validation/guard checks: - - 400: amount must be > 0 - - 400: Invalid timestamp - - 400: Unknown charge type -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (amount must be > 0; Invalid timestamp) - -## `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt` - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:86`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments` - -- Handler: `create` (`src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:47`) -- Behavior: Create resource (create). -- Path params: propertyId:UUID, bookingId:UUID -- Body: PaymentCreateRequest { amount:Long, method:String? (optional), currency:String? (optional), reference:String? (optional), notes:String? (optional), receivedAt:String? (optional) } -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `201` `PaymentResponse` -- Validation/guard checks: - - 400: amount must be > 0 - - 400: Invalid timestamp - - 400: Unknown payment method -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Booking not found), 400 (amount must be > 0; Invalid timestamp) - -### `DELETE /properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}` - -- Handler: `delete` (`src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:103`) -- Behavior: Delete resource (delete). -- Path params: propertyId:UUID, bookingId:UUID, paymentId:UUID -- Body: none -- Side effects: Emits booking SSE updates. -- Auth: Roles: ADMIN -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Cash payments can only be deleted for OPEN or CHECKED_IN bookings - - 400: Only CASH payments can be deleted -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Cash payments can only be deleted for OPEN or CHECKED_IN bookings; Only CASH payments can be deleted) - -## `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt` - -### `GET /properties/{propertyId}/cancellation-policy` - -- Handler: `get` (`src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:34`) -- Behavior: Get resource (get). -- Path params: propertyId:UUID -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `CancellationPolicyResponse` -- Common errors: none observed in controller checks - -### `PUT /properties/{propertyId}/cancellation-policy` - -- Handler: `upsert` (`src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:53`) -- Behavior: Upsert. -- Path params: propertyId:UUID -- Body: CancellationPolicyUpsertRequest { freeDaysBeforeCheckin:Int (optional), penaltyMode:String } -- Auth: Roles: ADMIN -- Response: `200` `CancellationPolicyResponse` -- Validation/guard checks: - - 400: freeDaysBeforeCheckin must be >= 0 - - 400: Unknown penaltyMode -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (freeDaysBeforeCheckin must be >= 0; Unknown penaltyMode) - -## `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt` - -### `GET /properties` - -- Handler: `listProperties` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:92`) -- Behavior: List resources (list properties). -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `List` -- Common errors: 401 (User not found) - -### `POST /properties` - -- Handler: `createProperty` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:53`) -- Behavior: Create resource (create property). -- Body: PropertyCreateRequest { name:String, addressText:String? (optional), timezone:String? (optional), currency:String? (optional), billingCheckinTime:String? (optional), billingCheckoutTime:String? (optional), active:Boolean? (optional), otaAliases:Set? (optional), emailAddresses:Set? (optional), allowedTransportModes:Set? (optional) } -- Auth: Roles: ADMIN -- Response: `201` `PropertyResponse` -- Validation/guard checks: - - 400: Unknown transport mode - - 400: $fieldName must be HH:mm - - 409: Unable to generate property code -- Common errors: 401 (User id missing; User not found), 400 (Unknown transport mode; $fieldName must be HH:mm), 409 (Unable to generate property code) - -### `PUT /properties/{propertyId}` - -- Handler: `updateProperty` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:306`) -- Behavior: Update resource (update property). -- Path params: propertyId:UUID -- Body: PropertyUpdateRequest { code:String, name:String, addressText:String? (optional), timezone:String? (optional), currency:String? (optional), billingCheckinTime:String? (optional), billingCheckoutTime:String? (optional), active:Boolean? (optional), otaAliases:Set? (optional), emailAddresses:Set? (optional), allowedTransportModes:Set? (optional) } -- Auth: Roles: ADMIN -- Response: `200` `PropertyResponse` -- Validation/guard checks: - - 400: Unknown transport mode - - 400: $fieldName must be HH:mm - - 409: Property code already exists -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Unknown transport mode; $fieldName must be HH:mm), 409 (Property code already exists) - -### `GET /properties/{propertyId}/billing-policy` - -- Handler: `getBillingPolicy` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:118`) -- Behavior: Get resource (get billing policy). -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `PropertyBillingPolicyResponse` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found) - -### `PUT /properties/{propertyId}/billing-policy` - -- Handler: `updateBillingPolicy` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:135`) -- Behavior: Update resource (update billing policy). -- Path params: propertyId:UUID -- Body: PropertyBillingPolicyRequest { billingCheckinTime:String, billingCheckoutTime:String } -- Auth: Roles: ADMIN -- Response: `200` `PropertyBillingPolicyResponse` -- Validation/guard checks: - - 400: $fieldName must be HH:mm -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 ($fieldName must be HH:mm) - -### `GET /properties/{propertyId}/code` - -- Handler: `getPropertyCode` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:105`) -- Behavior: Get resource (get property code). -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `PropertyCodeResponse` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found) - -### `GET /properties/{propertyId}/users` - -- Handler: `listPropertyUsers` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:165`) -- Behavior: List resources (list property users). -- Path params: propertyId:UUID -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `DELETE /properties/{propertyId}/users/{userId}` - -- Handler: `deletePropertyUser` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:290`) -- Behavior: Delete resource (delete property user). -- Path params: propertyId:UUID, userId:UUID -- Body: none -- Auth: Roles: ADMIN -- Response: `204` `Unit` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `PUT /properties/{propertyId}/users/{userId}/disabled` - -- Handler: `updatePropertyUserDisabled` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:240`) -- Behavior: Update resource (update property user disabled). -- Path params: propertyId:UUID, userId:UUID -- Body: PropertyUserDisableRequest { disabled:Boolean } -- Auth: Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR -- Response: `200` `PropertyUserResponse` -- Common errors: 401 (Missing principal), 403 (Role not allowed; Property membership required), 404 (User not found in property) - -### `PUT /properties/{propertyId}/users/{userId}/roles` - -- Handler: `upsertPropertyUserRoles` (`src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:189`) -- Behavior: Upsert property user roles. -- Path params: propertyId:UUID, userId:UUID -- Body: PropertyUserRoleRequest { roles:Set } -- Auth: Roles: ADMIN, AGENT, MANAGER, STAFF -- Response: `200` `PropertyUserResponse` -- Validation/guard checks: - - 400: Unknown role -- Common errors: 401 (Missing principal), 403 (Missing role; Role not allowed), 404 (Property not found; User not found), 400 (Unknown role) - -## `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt` - -### `POST /properties/access-codes/join` - -- Handler: `joinWithAccessCode` (`src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:91`) -- Behavior: Join with access code. -- Body: PropertyAccessCodeJoinRequest { propertyCode:String? (optional), propertyId:String? (optional), code:String } -- Auth: Authenticated user (Firebase) -- Response: `200` `PropertyUserResponse` -- Validation/guard checks: - - 400: Property code required - - 409: User already a member -- Common errors: 401 (User not found; Missing principal), 404 (Invalid code; Property not found), 400 (Property code required), 409 (User already a member) - -### `POST /properties/{propertyId}/access-codes` - -- Handler: `createAccessCode` (`src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:45`) -- Behavior: Create resource (create access code). -- Path params: propertyId:UUID -- Body: PropertyAccessCodeCreateRequest { roles:Set } -- Auth: Roles: ADMIN -- Response: `201` `PropertyAccessCodeResponse` -- Validation/guard checks: - - 400: ADMIN cannot be invited by code - - 400: At least one role is required - - 400: Unknown role - - 409: Unable to generate code, try again -- Common errors: 401 (User not found; Missing principal), 403 (Property membership required), 404 (Property not found), 400 (ADMIN cannot be invited by code; At least one role is required), 409 (Unable to generate code, try again) - -## `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt` - -### `GET /properties/{propertyId}/users/search` - -- Handler: `searchPropertyUsers` (`src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:50`) -- Behavior: Search property users. -- Path params: propertyId:UUID -- Query params: phone:String? (optional) -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `GET /users` - -- Handler: `listAppUsers` (`src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:27`) -- Behavior: List resources (list app users). -- Query params: phone:String? (optional) -- Body: none -- Auth: SUPER_ADMIN -- Response: `200` `List` -- Common errors: 401 (User not found), 403 (Super admin only) - -## `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt` - -### `GET /properties/{propertyId}/rate-plans` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:79`) -- Behavior: List resources (list). -- Path params: propertyId:UUID -- Query params: roomTypeCode:String? (optional) -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `POST /properties/{propertyId}/rate-plans` - -- Handler: `create` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:51`) -- Behavior: Create resource (create). -- Path params: propertyId:UUID -- Body: RatePlanCreateRequest { code:String, name:String, roomTypeCode:String, baseRate:Long, currency:String? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `RatePlanResponse` -- Validation/guard checks: - - 409: Rate plan code already exists for room type -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Room type not found), 409 (Rate plan code already exists for room type) - -### `DELETE /properties/{propertyId}/rate-plans/{ratePlanId}` - -- Handler: `delete` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:114`) -- Behavior: Delete resource (delete). -- Path params: propertyId:UUID, ratePlanId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found) - -### `PUT /properties/{propertyId}/rate-plans/{ratePlanId}` - -- Handler: `update` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:95`) -- Behavior: Update resource (update). -- Path params: propertyId:UUID, ratePlanId:UUID -- Body: RatePlanUpdateRequest { name:String, baseRate:Long, currency:String? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `RatePlanResponse` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found) - -### `GET /properties/{propertyId}/rate-plans/{ratePlanId}/calendar` - -- Handler: `listCalendar` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:170`) -- Behavior: List resources (list calendar). -- Path params: propertyId:UUID, ratePlanId:UUID -- Query params: from:String (required), to:String (required) -- Body: none -- Auth: Any property member -- Response: `200` `RateCalendarAverageResponse` -- Validation/guard checks: - - 400: to must be on/after from - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Rate plan not found), 400 (to must be on/after from; Invalid date format) - -### `POST /properties/{propertyId}/rate-plans/{ratePlanId}/calendar` - -- Handler: `upsertCalendar` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:129`) -- Behavior: Upsert calendar. -- Path params: propertyId:UUID, ratePlanId:UUID -- Body: RateCalendarRangeUpsertRequest { from:String, to:String, rate:Long } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `List` -- Validation/guard checks: - - 400: to must be on/after from - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found), 400 (to must be on/after from; Invalid date format) - -### `DELETE /properties/{propertyId}/rate-plans/{ratePlanId}/calendar/{rateDate}` - -- Handler: `deleteCalendar` (`src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:206`) -- Behavior: Delete resource (delete calendar). -- Path params: propertyId:UUID, ratePlanId:UUID, rateDate:String -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found), 400 (Invalid date format) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentLinksController.kt` - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/payment-link` - -- Handler: `createPaymentLink` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentLinksController.kt:47`) -- Behavior: Create resource (create payment link). -- Path params: propertyId:UUID, bookingId:UUID -- Body: RazorpayPaymentLinkCreateRequest { amount:Long? (optional), isPartialPaymentAllowed:Boolean? (optional), minAmountForCustomer:Long? (optional), description:String? (optional), expiryDate:String? (optional), successUrl:String? (optional), failureUrl:String? (optional), viaEmail:Boolean? (optional), viaSms:Boolean? (optional) } -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayPaymentLinkCreateResponse` -- Validation/guard checks: - - 400: Booking is not active - - 400: Razorpay settings not configured - - 400: amount must be > 0 - - 400: Razorpay test keys not configured -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Booking is not active; Razorpay settings not configured), 502 (Razorpay request failed) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt` - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/close` - -- Handler: `closeRequest` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:93`) -- Behavior: Close request. -- Path params: propertyId:UUID, bookingId:UUID -- Body: RazorpayPaymentRequestCloseRequest { qrId:String? (optional), paymentLinkId:String? (optional) } -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayPaymentRequestCloseResponse` -- Validation/guard checks: - - 400: Provide exactly one of qrId or paymentLinkId - - 400: Razorpay settings not configured - - 400: Razorpay test keys not configured -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Provide exactly one of qrId or paymentLinkId; Razorpay settings not configured), 502 (Razorpay close request failed; Razorpay cancel request failed) - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/requests` - -- Handler: `listRequests` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:45`) -- Behavior: List resources (list requests). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt` - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` - -- Handler: `listQr` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:292`) -- Behavior: List resources (list qr). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` - -- Handler: `createQr` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:56`) -- Behavior: Create resource (create qr). -- Path params: propertyId:UUID, bookingId:UUID -- Body: RazorpayQrGenerateRequest { amount:Long? (optional), customerName:String? (optional), customerEmail:String? (optional), customerPhone:String? (optional), expiryMinutes:Int? (optional), expirySeconds:Int? (optional) } -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayQrGenerateResponse` -- Validation/guard checks: - - 400: Booking is not active - - 400: Razorpay settings not configured - - 400: amount must be > 0 - - 400: Razorpay test keys not configured -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Booking is not active; Razorpay settings not configured), 502 (Razorpay request failed) - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/active` - -- Handler: `getActiveQr` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:157`) -- Behavior: Get resource (get active qr). -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayQrGenerateResponse?` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/close` - -- Handler: `closeActiveQr` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:180`) -- Behavior: Close active qr. -- Path params: propertyId:UUID, bookingId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayQrGenerateResponse?` -- Validation/guard checks: - - 400: Razorpay settings not configured - - 400: Razorpay test keys not configured -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Razorpay settings not configured; Razorpay test keys not configured), 502 (Razorpay close request failed) - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close` - -- Handler: `closeQrById` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:212`) -- Behavior: Close qr by id. -- Path params: propertyId:UUID, bookingId:UUID, qrId:String -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `RazorpayQrGenerateResponse?` -- Validation/guard checks: - - 400: Razorpay settings not configured - - 400: Razorpay test keys not configured -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Razorpay settings not configured; Razorpay test keys not configured), 502 (Razorpay close request failed) - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events` - -- Handler: `qrEvents` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:244`) -- Behavior: Qr events. -- Path params: propertyId:UUID, qrId:String -- Body: none -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Required property role not granted) - -### `GET /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events/stream` - -- Handler: `streamQrEvents` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:273`) -- Behavior: Stream events/data (stream qr events). -- Path params: propertyId:UUID, bookingId:UUID, qrId:String -- Body: none -- Side effects: Streams SSE events. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `SseEmitter` -- Common errors: 401 (Missing principal), 403 (Required property role not granted) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayRefundsController.kt` - -### `POST /properties/{propertyId}/bookings/{bookingId}/payments/razorpay/refund` - -- Handler: `refund` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayRefundsController.kt:42`) -- Behavior: Refund. -- Path params: propertyId:UUID, bookingId:UUID -- Body: RazorpayRefundRequest { paymentId:UUID? (optional), amount:Long? (optional), notes:String? (optional) } -- Auth: Roles: ADMIN, FINANCE, MANAGER -- Response: `200` `RazorpayRefundResponse` -- Validation/guard checks: - - 400: paymentId is required - - 400: amount must be <= payment amount - - 400: Payment is missing gateway id - - 400: Payment is not a Razorpay payment -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (paymentId is required; amount must be <= payment amount), 502 (Razorpay refund request failed) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt` - -### `POST /properties/{propertyId}/razorpay/return/failure` - -- Handler: `failure` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:22`) -- Behavior: Failure. -- Path params: propertyId:UUID -- Body: none -- Auth: Public/unspecified -- Response: `204` `Unit` -- Common errors: none observed in controller checks - -### `POST /properties/{propertyId}/razorpay/return/success` - -- Handler: `success` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:16`) -- Behavior: Success. -- Path params: propertyId:UUID -- Body: none -- Auth: Public/unspecified -- Response: `204` `Unit` -- Common errors: none observed in controller checks - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt` - -### `GET /properties/{propertyId}/razorpay-settings` - -- Handler: `getSettings` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:33`) -- Behavior: Get resource (get settings). -- Path params: propertyId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `RazorpaySettingsResponse` -- Common errors: 401 (Missing principal), 403 (Required property role not granted) - -### `PUT /properties/{propertyId}/razorpay-settings` - -- Handler: `upsertSettings` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:57`) -- Behavior: Upsert settings. -- Path params: propertyId:UUID -- Body: RazorpaySettingsUpsertRequest { keyId:String? (optional), keySecret:String? (optional), webhookSecret:String? (optional), keyIdTest:String? (optional), keySecretTest:String? (optional), webhookSecretTest:String? (optional), isTest:Boolean? (optional), merchantKey:String? (optional), salt32:String? (optional), salt256:String? (optional), useSalt256:Boolean? (optional) } -- Auth: Roles: ADMIN -- Response: `200` `RazorpaySettingsResponse` -- Validation/guard checks: - - 400: keyId and keySecret must be provided together - - 400: keyIdTest and keySecretTest must be provided together - - 400: keyId/keySecret required - - 400: keyIdTest/keySecretTest required when isTest=true -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (keyId and keySecret must be provided together; keyIdTest and keySecretTest must be provided together) - -## `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayWebhookCapture.kt` - -### `POST /properties/{propertyId}/razorpay/webhook` - -- Handler: `capture` (`src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayWebhookCapture.kt:53`) -- Behavior: Capture. -- Path params: propertyId:UUID -- Body: String? -- Side effects: Emits booking SSE updates. -- Auth: Public/unspecified -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Razorpay settings not configured - - 400: Webhook secret not configured -- Common errors: 401 (Missing signature; Invalid signature), 404 (Property not found), 400 (Razorpay settings not configured; Webhook secret not configured) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt` - -### `GET /amenities` - -- Handler: `listAmenities` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:40`) -- Behavior: List resources (list amenities). -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `List` -- Common errors: 401 (Missing principal) - -### `POST /amenities` - -- Handler: `createAmenity` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:49`) -- Behavior: Create resource (create amenity). -- Body: AmenityUpsertRequest { name:String, category:String? (optional), iconKey:String? (optional) } -- Auth: SUPER_ADMIN -- Response: `201` `AmenityResponse` -- Validation/guard checks: - - 400: Icon key not found - - 409: Amenity already exists -- Common errors: 401 (User not found), 403 (Super admin only), 400 (Icon key not found), 409 (Amenity already exists) - -### `DELETE /amenities/{amenityId}` - -- Handler: `deleteAmenity` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:92`) -- Behavior: Delete resource (delete amenity). -- Path params: amenityId:UUID -- Body: none -- Auth: SUPER_ADMIN -- Response: `204` `Unit` -- Common errors: 401 (User not found), 403 (Super admin only), 404 (Amenity not found) - -### `PUT /amenities/{amenityId}` - -- Handler: `updateAmenity` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:68`) -- Behavior: Update resource (update amenity). -- Path params: amenityId:UUID -- Body: AmenityUpsertRequest { name:String, category:String? (optional), iconKey:String? (optional) } -- Auth: SUPER_ADMIN -- Response: `200` `AmenityResponse` -- Validation/guard checks: - - 400: Icon key not found - - 409: Amenity already exists -- Common errors: 401 (User not found), 403 (Super admin only), 404 (Amenity not found), 400 (Icon key not found), 409 (Amenity already exists) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt` - -### `GET /image-tags` - -- Handler: `listTags` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:35`) -- Behavior: List resources (list tags). -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `List` -- Common errors: none observed in controller checks - -### `POST /image-tags` - -- Handler: `createTag` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:43`) -- Behavior: Create resource (create tag). -- Body: RoomImageTagUpsertRequest { name:String } -- Auth: SUPER_ADMIN -- Response: `201` `RoomImageTagResponse` -- Validation/guard checks: - - 409: Tag already exists -- Common errors: 401 (User not found), 403 (Super admin only), 409 (Tag already exists) - -### `DELETE /image-tags/{tagId}` - -- Handler: `deleteTag` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:74`) -- Behavior: Delete resource (delete tag). -- Path params: tagId:UUID -- Body: none -- Auth: SUPER_ADMIN -- Response: `204` `Unit` -- Common errors: 401 (User not found), 403 (Super admin only), 404 (Tag not found) - -### `PUT /image-tags/{tagId}` - -- Handler: `updateTag` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:56`) -- Behavior: Update resource (update tag). -- Path params: tagId:UUID -- Body: RoomImageTagUpsertRequest { name:String } -- Auth: SUPER_ADMIN -- Response: `200` `RoomImageTagResponse` -- Validation/guard checks: - - 409: Tag already exists -- Common errors: 401 (User not found), 403 (Super admin only), 404 (Tag not found), 409 (Tag already exists) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt` - -### `GET /properties/{propertyId}/rooms/{roomId}/images` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:55`) -- Behavior: List resources (list). -- Path params: propertyId:UUID, roomId:UUID -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `List` -- Common errors: 404 (Room not found) - -### `POST /properties/{propertyId}/rooms/{roomId}/images` - -- Handler: `upload` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:85`) -- Behavior: Upload. -- Path params: propertyId:UUID, roomId:UUID -- Query params: file:MultipartFile (required), tagIds:List? (optional) -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `RoomImageResponse` -- Validation/guard checks: - - 400: File is empty - - 409: Duplicate image for room -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Tag not found; Room not found), 400 (File is empty), 409 (Duplicate image for room) - -### `PUT /properties/{propertyId}/rooms/{roomId}/images/reorder-room` - -- Handler: `reorderRoomImages` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:199`) -- Behavior: Reorder room images. -- Path params: propertyId:UUID, roomId:UUID -- Body: RoomImageReorderRequest { imageIds:List } -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Images do not belong to room -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 400 (Images do not belong to room) - -### `PUT /properties/{propertyId}/rooms/{roomId}/images/reorder-room-type` - -- Handler: `reorderRoomTypeImages` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:227`) -- Behavior: Reorder room type images. -- Path params: propertyId:UUID, roomId:UUID -- Body: RoomImageReorderRequest { imageIds:List } -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Validation/guard checks: - - 400: Images do not belong to room type - - 400: Images do not belong to property -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 400 (Images do not belong to room type; Images do not belong to property) - -### `DELETE /properties/{propertyId}/rooms/{roomId}/images/{imageId}` - -- Handler: `delete` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:129`) -- Behavior: Delete resource (delete). -- Path params: propertyId:UUID, roomId:UUID, imageId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 500 (Failed to delete image files) - -### `GET /properties/{propertyId}/rooms/{roomId}/images/{imageId}/file` - -- Handler: `file` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:257`) -- Behavior: File. -- Path params: propertyId:UUID, roomId:UUID, imageId:UUID -- Query params: size:String (optional) -- Body: none -- Auth: Any property member -- Response: `200` `ResponseEntity` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Image not found; File missing) - -### `PUT /properties/{propertyId}/rooms/{roomId}/images/{imageId}/tags` - -- Handler: `updateTags` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:180`) -- Behavior: Update resource (update tags). -- Path params: propertyId:UUID, roomId:UUID, imageId:UUID -- Body: RoomImageTagUpdateRequest { tagIds:Set } -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Tag not found) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt` - -### `GET /properties/{propertyId}/room-stays/active` - -- Handler: `listActiveRoomStays` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:42`) -- Behavior: List resources (list active room stays). -- Path params: propertyId:UUID -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Agents cannot view active stays; Property membership required) - -### `POST /properties/{propertyId}/room-stays/{roomStayId}/void` - -- Handler: `voidRoomStay` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:78`) -- Behavior: Void room stay. -- Path params: propertyId:UUID, roomStayId:UUID -- Body: RoomStayVoidRequest { reason:String? (optional) } -- Side effects: Writes room-stay audit log. -- Auth: Roles: ADMIN, MANAGER, STAFF -- Response: `200` `Unit` -- Validation/guard checks: - - 409: Cannot void checked-out room stay -- Common errors: 401 (Missing principal), 403 (Missing role; Cannot void stay after first payment), 404 (Room stay not found for property), 409 (Cannot void checked-out room stay) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypeImages.kt` - -### `GET /properties/{propertyId}/room-types/{roomTypeCode}/images` - -- Handler: `listByRoomType` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypeImages.kt:29`) -- Behavior: List resources (list by room type). -- Path params: propertyId:UUID, roomTypeCode:String -- Body: none -- Auth: Public/unspecified -- Response: `200` `List` -- Common errors: 404 (Room type not found) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt` - -### `GET /properties/{propertyId}/room-types` - -- Handler: `listRoomTypes` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:49`) -- Behavior: List resources (list room types). -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `POST /properties/{propertyId}/room-types` - -- Handler: `createRoomType` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:107`) -- Behavior: Create resource (create room type). -- Path params: propertyId:UUID -- Body: RoomTypeUpsertRequest { code:String, name:String, baseOccupancy:Int? (optional), maxOccupancy:Int? (optional), sqFeet:Int? (optional), bathroomSqFeet:Int? (optional), defaultRate:Long? (optional), active:Boolean? (optional), otaAliases:Set? (optional), amenityIds:Set? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `201` `RoomTypeResponse` -- Validation/guard checks: - - 409: Room type code already exists for property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found; Amenity not found), 409 (Room type code already exists for property) - -### `GET /properties/{propertyId}/room-types/{roomTypeCode}/rate` - -- Handler: `resolveRate` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:60`) -- Behavior: Resolve rate. -- Path params: propertyId:UUID, roomTypeCode:String -- Query params: date:String (required), ratePlanCode:String? (optional) -- Body: none -- Auth: Any property member -- Response: `200` `RateResolveResponse` -- Validation/guard checks: - - 400: Rate plan not for room type - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found; Room type not found), 400 (Rate plan not for room type; Invalid date format) - -### `DELETE /properties/{propertyId}/room-types/{roomTypeId}` - -- Handler: `deleteRoomType` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:189`) -- Behavior: Delete resource (delete room type). -- Path params: propertyId:UUID, roomTypeId:UUID -- Body: none -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Room type not found) - -### `PUT /properties/{propertyId}/room-types/{roomTypeId}` - -- Handler: `updateRoomType` (`src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:153`) -- Behavior: Update resource (update room type). -- Path params: propertyId:UUID, roomTypeId:UUID -- Body: RoomTypeUpsertRequest { code:String, name:String, baseOccupancy:Int? (optional), maxOccupancy:Int? (optional), sqFeet:Int? (optional), bathroomSqFeet:Int? (optional), defaultRate:Long? (optional), active:Boolean? (optional), otaAliases:Set? (optional), amenityIds:Set? (optional) } -- Auth: Roles: ADMIN, MANAGER -- Response: `200` `RoomTypeResponse` -- Validation/guard checks: - - 409: Room type code already exists for property -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Room type not found; Amenity not found), 409 (Room type code already exists for property) - -## `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt` - -### `GET /properties/{propertyId}/rooms` - -- Handler: `listRooms` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:66`) -- Behavior: List resources (list rooms). -- Path params: propertyId:UUID -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `POST /properties/{propertyId}/rooms` - -- Handler: `createRoom` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:278`) -- Behavior: Create resource (create room). -- Path params: propertyId:UUID -- Body: RoomUpsertRequest { roomNumber:Int, floor:Int? (optional), roomTypeCode:String, hasNfc:Boolean, active:Boolean, maintenance:Boolean, notes:String? (optional) } -- Side effects: Emits room board SSE updates. -- Auth: Roles: ADMIN -- Response: `201` `RoomResponse` -- Validation/guard checks: - - 400: roomTypeCode required - - 409: Room number already exists for property -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Room type not found), 400 (roomTypeCode required), 409 (Room number already exists for property) - -### `GET /properties/{propertyId}/rooms/availability` - -- Handler: `roomAvailability` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:126`) -- Behavior: Room availability. -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `GET /properties/{propertyId}/rooms/availability-range` - -- Handler: `roomAvailabilityRange` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:184`) -- Behavior: Room availability range. -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Validation/guard checks: - - 400: Invalid date range - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Invalid date range; Invalid date format) - -### `GET /properties/{propertyId}/rooms/available` - -- Handler: `availableRooms` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:147`) -- Behavior: Available rooms. -- Path params: propertyId:UUID -- Body: none -- Auth: Authenticated user (Firebase) -- Response: `200` `List` -- Common errors: none observed in controller checks - -### `GET /properties/{propertyId}/rooms/available-range-with-rate` - -- Handler: `availableRoomsWithRate` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:221`) -- Behavior: Available rooms with rate. -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Validation/guard checks: - - 400: Invalid date range - - 400: Invalid date format -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Invalid date range; Invalid date format) - -### `GET /properties/{propertyId}/rooms/board` - -- Handler: `roomBoard` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:86`) -- Behavior: Room board. -- Path params: propertyId:UUID -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `GET /properties/{propertyId}/rooms/board/stream` - -- Handler: `roomBoardStream` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:114`) -- Behavior: Room board stream. -- Path params: propertyId:UUID -- Body: none -- Side effects: Streams SSE events. -- Auth: Any property member -- Response: `200` `SseEmitter` -- Common errors: 401 (Missing principal), 403 (Property membership required) - -### `GET /properties/{propertyId}/rooms/by-type/{roomTypeCode}` - -- Handler: `roomsByType` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:160`) -- Behavior: Rooms by type. -- Path params: propertyId:UUID, roomTypeCode:String -- Body: none -- Auth: Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF -- Response: `200` `List` -- Common errors: 404 (Room type not found) - -### `DELETE /properties/{propertyId}/rooms/{roomId}` - -- Handler: `deleteRoom` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:367`) -- Behavior: Delete resource (delete room). -- Path params: propertyId:UUID, roomId:UUID -- Body: none -- Side effects: Emits room board SSE updates. -- Auth: Roles: ADMIN, MANAGER -- Response: `204` `Unit` -- Validation/guard checks: - - 409: Cannot delete room with stays -- Common errors: 401 (Missing principal), 403 (Property membership required), 404 (Room not found for property), 409 (Cannot delete room with stays), 500 (Failed to delete room image files) - -### `PUT /properties/{propertyId}/rooms/{roomId}` - -- Handler: `updateRoom` (`src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:323`) -- Behavior: Update resource (update room). -- Path params: propertyId:UUID, roomId:UUID -- Body: RoomUpsertRequest { roomNumber:Int, floor:Int? (optional), roomTypeCode:String, hasNfc:Boolean, active:Boolean, maintenance:Boolean, notes:String? (optional) } -- Side effects: Emits room board SSE updates. -- Auth: Roles: ADMIN -- Response: `200` `RoomResponse` -- Validation/guard checks: - - 400: roomTypeCode required - - 409: Room number already exists for property -- Common errors: 401 (Missing principal), 403 (Required property role not granted), 404 (Room not found for property; Room type not found), 400 (roomTypeCode required), 409 (Room number already exists for property) - -## `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt` - -### `GET /` - -- Handler: `root` (`src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:14`) -- Behavior: Root. -- Body: none -- Auth: Public/unspecified -- Response: `200` `Map` -- Common errors: none observed in controller checks - -### `GET /health` - -- Handler: `health` (`src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:9`) -- Behavior: Health. -- Body: none -- Auth: Public/unspecified -- Response: `200` `Map` -- Common errors: none observed in controller checks - -## `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt` - -### `GET /properties/{propertyId}/transport-modes` - -- Handler: `list` (`src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:26`) -- Behavior: List resources (list). -- Path params: propertyId:UUID -- Body: none -- Auth: Any property member -- Response: `200` `List` -- Common errors: 401 (Missing principal), 403 (Property membership required) +- Scope: endpoints declared in `src/main/kotlin/com/android/trisolarisserver/controller`. +- Columns: method, path, handler (controller function). + +## System + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/` | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:13` (`root`) | +| `GET` | `/health` | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:8` (`health`) | + +## Auth + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/auth/me` | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:43` (`me`) | +| `PUT` | `/auth/me` | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:53` (`updateMe`) | +| `POST` | `/auth/verify` | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:32` (`verify`) | + +## Properties & Users + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:91` (`listProperties`) | +| `POST` | `/properties` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:51` (`createProperty`) | +| `POST` | `/properties/access-codes/join` | `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:89` (`joinWithAccessCode`) | +| `PUT` | `/properties/{propertyId}` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:305` (`updateProperty`) | +| `POST` | `/properties/{propertyId}/access-codes` | `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:43` (`createAccessCode`) | +| `GET` | `/properties/{propertyId}/billing-policy` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:117` (`getBillingPolicy`) | +| `PUT` | `/properties/{propertyId}/billing-policy` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:134` (`updateBillingPolicy`) | +| `GET` | `/properties/{propertyId}/bookings` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:186` (`listBookings`) | +| `POST` | `/properties/{propertyId}/bookings` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:96` (`createBooking`) | +| `GET` | `/properties/{propertyId}/code` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:104` (`getPropertyCode`) | +| `GET` | `/properties/{propertyId}/razorpay-settings` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:32` (`getSettings`) | +| `PUT` | `/properties/{propertyId}/razorpay-settings` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:56` (`upsertSettings`) | +| `POST` | `/properties/{propertyId}/razorpay/return/failure` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:20` (`failure`) | +| `POST` | `/properties/{propertyId}/razorpay/return/success` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:14` (`success`) | +| `POST` | `/properties/{propertyId}/razorpay/webhook` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayWebhookCapture.kt:50` (`capture`) | +| `GET` | `/properties/{propertyId}/users` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:164` (`listPropertyUsers`) | +| `GET` | `/properties/{propertyId}/users/search` | `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:49` (`searchPropertyUsers`) | +| `DELETE` | `/properties/{propertyId}/users/{userId}` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:288` (`deletePropertyUser`) | +| `PUT` | `/properties/{propertyId}/users/{userId}/disabled` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:239` (`updatePropertyUserDisabled`) | +| `PUT` | `/properties/{propertyId}/users/{userId}/roles` | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:188` (`upsertPropertyUserRoles`) | +| `GET` | `/users` | `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:26` (`listAppUsers`) | + +## Bookings + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/bookings/{bookingId}` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:280` (`getBooking`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/balance` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingBalances.kt:31` (`getBalance`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/billing-policy` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:473` (`updateBillingPolicy`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/cancel` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:692` (`cancel`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/charges` | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:79` (`list`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/charges` | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:41` (`create`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/check-in/bulk` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:350` (`bulkCheckIn`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/check-out` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:524` (`checkOut`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/expected-dates` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:427` (`updateExpectedDates`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/link-guest` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:323` (`linkGuest`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/no-show` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:717` (`noShow`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments` | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:85` (`list`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments` | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:45` (`create`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/close` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:92` (`closeRequest`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/payment-link` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentLinksController.kt:45` (`createPaymentLink`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:291` (`listQr`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:54` (`createQr`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/active` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:156` (`getActiveQr`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/close` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:178` (`closeActiveQr`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:210` (`closeQrById`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:243` (`qrEvents`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events/stream` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:272` (`streamQrEvents`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/refund` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayRefundsController.kt:41` (`refund`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/requests` | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:44` (`listRequests`) | +| `DELETE` | `/properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}` | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:101` (`delete`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/room-requests` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:102` (`list`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/room-requests` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:45` (`create`) | +| `DELETE` | `/properties/{propertyId}/bookings/{bookingId}/room-requests/{requestId}` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:118` (`cancel`) | +| `POST` | `/properties/{propertyId}/bookings/{bookingId}/room-stays/{roomStayId}/check-out` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:580` (`checkOutRoomStay`) | +| `GET` | `/properties/{propertyId}/bookings/{bookingId}/stream` | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:301` (`streamBooking`) | + +## Guests & Documents + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/guests/search` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:74` (`search`) | +| `GET` | `/properties/{propertyId}/guests/visit-count` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:112` (`getVisitCount`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:101` (`getGuest`) | +| `PUT` | `/properties/{propertyId}/guests/{guestId}` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:44` (`updateGuest`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}/documents` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:124` (`listDocuments`) | +| `POST` | `/properties/{propertyId}/guests/{guestId}/documents` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:63` (`uploadDocument`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}/documents/stream` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:137` (`streamDocuments`) | +| `DELETE` | `/properties/{propertyId}/guests/{guestId}/documents/{documentId}` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:181` (`deleteDocument`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}/documents/{documentId}/file` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:151` (`downloadDocument`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}/ratings` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:78` (`list`) | +| `POST` | `/properties/{propertyId}/guests/{guestId}/ratings` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:40` (`create`) | +| `POST` | `/properties/{propertyId}/guests/{guestId}/signature` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:168` (`uploadSignature`) | +| `GET` | `/properties/{propertyId}/guests/{guestId}/signature/file` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:193` (`downloadSignature`) | +| `POST` | `/properties/{propertyId}/guests/{guestId}/vehicles` | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:128` (`addVehicle`) | + +## Rooms & Images + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/rooms` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:65` (`listRooms`) | +| `POST` | `/properties/{propertyId}/rooms` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:276` (`createRoom`) | +| `GET` | `/properties/{propertyId}/rooms/availability` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:125` (`roomAvailability`) | +| `GET` | `/properties/{propertyId}/rooms/availability-range` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:183` (`roomAvailabilityRange`) | +| `GET` | `/properties/{propertyId}/rooms/available` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:146` (`availableRooms`) | +| `GET` | `/properties/{propertyId}/rooms/available-range-with-rate` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:220` (`availableRoomsWithRate`) | +| `GET` | `/properties/{propertyId}/rooms/board` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:85` (`roomBoard`) | +| `GET` | `/properties/{propertyId}/rooms/board/stream` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:113` (`roomBoardStream`) | +| `GET` | `/properties/{propertyId}/rooms/by-type/{roomTypeCode}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:159` (`roomsByType`) | +| `DELETE` | `/properties/{propertyId}/rooms/{roomId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:365` (`deleteRoom`) | +| `PUT` | `/properties/{propertyId}/rooms/{roomId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:322` (`updateRoom`) | +| `POST` | `/properties/{propertyId}/rooms/{roomId}/cards/prepare-temp` | `src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:43` (`prepareTemporary`) | +| `POST` | `/properties/{propertyId}/rooms/{roomId}/cards/temp` | `src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:71` (`issueTemporary`) | +| `GET` | `/properties/{propertyId}/rooms/{roomId}/images` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:54` (`list`) | +| `POST` | `/properties/{propertyId}/rooms/{roomId}/images` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:83` (`upload`) | +| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/reorder-room` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:196` (`reorderRoomImages`) | +| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/reorder-room-type` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:224` (`reorderRoomTypeImages`) | +| `DELETE` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:126` (`delete`) | +| `GET` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}/file` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:256` (`file`) | +| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}/tags` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:177` (`updateTags`) | + +## Room Types + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/room-types` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:48` (`listRoomTypes`) | +| `POST` | `/properties/{propertyId}/room-types` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:105` (`createRoomType`) | +| `GET` | `/properties/{propertyId}/room-types/{roomTypeCode}/images` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypeImages.kt:28` (`listByRoomType`) | +| `GET` | `/properties/{propertyId}/room-types/{roomTypeCode}/rate` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:59` (`resolveRate`) | +| `DELETE` | `/properties/{propertyId}/room-types/{roomTypeId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:187` (`deleteRoomType`) | +| `PUT` | `/properties/{propertyId}/room-types/{roomTypeId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:152` (`updateRoomType`) | + +## Room Stays & Cards + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/room-stays/active` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:41` (`listActiveRoomStays`) | +| `GET` | `/properties/{propertyId}/room-stays/cards/{cardIndex}` | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:157` (`getCardByIndex`) | +| `POST` | `/properties/{propertyId}/room-stays/cards/{cardIndex}/revoke` | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:136` (`revoke`) | +| `GET` | `/properties/{propertyId}/room-stays/{roomStayId}/cards` | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:124` (`list`) | +| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/cards` | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:80` (`issue`) | +| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/cards/prepare` | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:47` (`prepare`) | +| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/void` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:77` (`voidRoomStay`) | + +## Rate Plans + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/rate-plans` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:78` (`list`) | +| `POST` | `/properties/{propertyId}/rate-plans` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:49` (`create`) | +| `DELETE` | `/properties/{propertyId}/rate-plans/{ratePlanId}` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:111` (`delete`) | +| `PUT` | `/properties/{propertyId}/rate-plans/{ratePlanId}` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:94` (`update`) | +| `GET` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:169` (`listCalendar`) | +| `POST` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:126` (`upsertCalendar`) | +| `DELETE` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar/{rateDate}` | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:204` (`deleteCalendar`) | + +## Cancellation Policy + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/cancellation-policy` | `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:33` (`get`) | +| `PUT` | `/properties/{propertyId}/cancellation-policy` | `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:52` (`upsert`) | + +## Transport Modes + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/properties/{propertyId}/transport-modes` | `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:25` (`list`) | + +## Inbound Email + +| Method | Path | Handler | +|---|---|---| +| `POST` | `/properties/{propertyId}/inbound-emails/manual` | `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmailManual.kt:39` (`uploadManualPdf`) | +| `GET` | `/properties/{propertyId}/inbound-emails/{emailId}/file` | `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmails.kt:31` (`downloadEmailPdf`) | + +## Assets & Metadata + +| Method | Path | Handler | +|---|---|---| +| `GET` | `/amenities` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:39` (`listAmenities`) | +| `POST` | `/amenities` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:47` (`createAmenity`) | +| `DELETE` | `/amenities/{amenityId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:89` (`deleteAmenity`) | +| `PUT` | `/amenities/{amenityId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:67` (`updateAmenity`) | +| `GET` | `/icons/png` | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:22` (`listPng`) | +| `GET` | `/icons/png/{filename}` | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:38` (`getPng`) | +| `GET` | `/image-tags` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:34` (`listTags`) | +| `POST` | `/image-tags` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:41` (`createTag`) | +| `DELETE` | `/image-tags/{tagId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:71` (`deleteTag`) | +| `PUT` | `/image-tags/{tagId}` | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:55` (`updateTag`) | diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md deleted file mode 100644 index e48dada..0000000 --- a/docs/API_REFERENCE.md +++ /dev/null @@ -1,144 +0,0 @@ -# API Reference - -Generated from controller source. - -- Total endpoints: **125** -- Auth: Firebase Bearer token unless endpoint is public. -- Regenerate: `python scripts/generate_api_docs.py` - -## Usage Template - -```bash -curl -X "https://api.hoteltrisolaris.in" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '' -``` - -| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Auth | Common Errors | Behavior | Handler | -|---|---|---|---|---|---|---|---|---|---|---| -| `GET` | `/` | `-` | `-` | `-` | `Map` | `200` | Public/unspecified | - | Root. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:14` (`root`) | -| `GET` | `/amenities` | `-` | `-` | `-` | `List` | `200` | Authenticated user (Firebase) | 401 (Missing principal) | List resources (list amenities). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:40` (`listAmenities`) | -| `POST` | `/amenities` | `-` | `-` | `AmenityUpsertRequest` | `AmenityResponse` | `201` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 400 (Icon key not found), 409 (Amenity already exists) | Create resource (create amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:49` (`createAmenity`) | -| `DELETE` | `/amenities/{amenityId}` | `amenityId:UUID` | `-` | `-` | `Unit` | `204` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 404 (Amenity not found) | Delete resource (delete amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:92` (`deleteAmenity`) | -| `PUT` | `/amenities/{amenityId}` | `amenityId:UUID` | `-` | `AmenityUpsertRequest` | `AmenityResponse` | `200` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 404 (Amenity not found), 400 (Icon key not found), 409 (Amenity already exists) | Update resource (update amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:68` (`updateAmenity`) | -| `GET` | `/auth/me` | `-` | `-` | `-` | `ResponseEntity` | `200` | Authenticated user (Firebase) | - | Me. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:44` (`me`) | -| `PUT` | `/auth/me` | `-` | `-` | `UpdateMeRequest` | `ResponseEntity` | `200` | Authenticated user (Firebase) | 401 (User not found) | Update resource (update me). | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:54` (`updateMe`) | -| `POST` | `/auth/verify` | `-` | `-` | `-` | `ResponseEntity` | `200` | Authenticated user (Firebase) | - | Verify. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:33` (`verify`) | -| `GET` | `/health` | `-` | `-` | `-` | `Map` | `200` | Public/unspecified | - | Health. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:9` (`health`) | -| `GET` | `/icons/png` | `-` | `-` | `-` | `List` | `200` | Public/unspecified | - | List resources (list png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:23` (`listPng`) | -| `GET` | `/icons/png/{filename}` | `filename:String` | `-` | `-` | `ResponseEntity` | `200` | Public/unspecified | - | Get resource (get png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:39` (`getPng`) | -| `GET` | `/image-tags` | `-` | `-` | `-` | `List` | `200` | Authenticated user (Firebase) | - | List resources (list tags). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:35` (`listTags`) | -| `POST` | `/image-tags` | `-` | `-` | `RoomImageTagUpsertRequest` | `RoomImageTagResponse` | `201` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 409 (Tag already exists) | Create resource (create tag). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:43` (`createTag`) | -| `DELETE` | `/image-tags/{tagId}` | `tagId:UUID` | `-` | `-` | `Unit` | `204` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 404 (Tag not found) | Delete resource (delete tag). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:74` (`deleteTag`) | -| `PUT` | `/image-tags/{tagId}` | `tagId:UUID` | `-` | `RoomImageTagUpsertRequest` | `RoomImageTagResponse` | `200` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only), 404 (Tag not found), 409 (Tag already exists) | Update resource (update tag). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:56` (`updateTag`) | -| `GET` | `/properties` | `-` | `-` | `-` | `List` | `200` | Authenticated user (Firebase) | 401 (User not found) | List resources (list properties). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:92` (`listProperties`) | -| `POST` | `/properties` | `-` | `-` | `PropertyCreateRequest` | `PropertyResponse` | `201` | Roles: ADMIN | 401 (User id missing; User not found), 400 (Unknown transport mode; $fieldName must be HH:mm), 409 (Unable to generate property code) | Create resource (create property). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:53` (`createProperty`) | -| `POST` | `/properties/access-codes/join` | `-` | `-` | `PropertyAccessCodeJoinRequest` | `PropertyUserResponse` | `200` | Authenticated user (Firebase) | 401 (User not found; Missing principal), 404 (Invalid code; Property not found), 400 (Property code required), 409 (User already a member) | Join with access code. | `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:91` (`joinWithAccessCode`) | -| `PUT` | `/properties/{propertyId}` | `propertyId:UUID` | `-` | `PropertyUpdateRequest` | `PropertyResponse` | `200` | Roles: ADMIN | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Unknown transport mode; $fieldName must be HH:mm), 409 (Property code already exists) | Update resource (update property). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:306` (`updateProperty`) | -| `POST` | `/properties/{propertyId}/access-codes` | `propertyId:UUID` | `-` | `PropertyAccessCodeCreateRequest` | `PropertyAccessCodeResponse` | `201` | Roles: ADMIN | 401 (User not found; Missing principal), 403 (Property membership required), 404 (Property not found), 400 (ADMIN cannot be invited by code; At least one role is required), 409 (Unable to generate code, try again) | Create resource (create access code). | `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:45` (`createAccessCode`) | -| `GET` | `/properties/{propertyId}/billing-policy` | `propertyId:UUID` | `-` | `-` | `PropertyBillingPolicyResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found) | Get resource (get billing policy). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:118` (`getBillingPolicy`) | -| `PUT` | `/properties/{propertyId}/billing-policy` | `propertyId:UUID` | `-` | `PropertyBillingPolicyRequest` | `PropertyBillingPolicyResponse` | `200` | Roles: ADMIN | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 ($fieldName must be HH:mm) | Update resource (update billing policy). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:135` (`updateBillingPolicy`) | -| `GET` | `/properties/{propertyId}/bookings` | `propertyId:UUID` | `status:String? (optional)` | `-` | `List` | `200` | Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (Invalid status: $value) | List resources (list bookings). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:187` (`listBookings`) | -| `POST` | `/properties/{propertyId}/bookings` | `propertyId:UUID` | `-` | `BookingCreateRequest` | `BookingCreateResponse` | `201` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (expectedCheckInAt required; expectedCheckOutAt required) | Create resource (create booking). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:99` (`createBooking`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `BookingDetailResponse` | `200` | Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | Get resource (get booking). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:282` (`getBooking`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/balance` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `BookingBalanceResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) | Get resource (get balance). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingBalances.kt:32` (`getBalance`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/billing-policy` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingBillingPolicyUpdateRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 ($fieldName required; $fieldName must be HH:mm), 409 (Booking closed) | Update resource (update billing policy). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:476` (`updateBillingPolicy`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/cancel` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingCancelRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Cannot cancel checked-in booking) | Cancel flow (cancel). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:695` (`cancel`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/charges` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:80` (`list`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/charges` | `propertyId:UUID, bookingId:UUID` | `-` | `ChargeCreateRequest` | `ChargeResponse` | `201` | Roles: ADMIN, FINANCE, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (amount must be > 0; Invalid timestamp) | Create resource (create). | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Charges.kt:44` (`create`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/check-in/bulk` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingBulkCheckInRequest` | `Unit` | `201` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Room not found; Booking not found), 400 (stays required; Duplicate roomId in stays), 409 (Booking not open; Room not available) | Bulk check in. | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:353` (`bulkCheckIn`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/check-out` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingCheckOutRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Invalid timestamp), 409 (Booking not checked in; Room stay amount is outside allowed range) | Check out flow (check out). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:527` (`checkOut`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/expected-dates` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingExpectedDatesUpdateRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Invalid date range; Invalid timestamp), 409 (Cannot change expected check-in after check-in; Booking closed) | Update resource (update expected dates). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:430` (`updateExpectedDates`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/link-guest` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingLinkGuestRequest` | `Unit` | `204` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Guest not found; Booking not found), 400 (Guest not in property) | Link guest. | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:326` (`linkGuest`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/no-show` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingNoShowRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Booking not open) | No-show flow (no show). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:720` (`noShow`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Booking not found for property) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:86` (`list`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments` | `propertyId:UUID, bookingId:UUID` | `-` | `PaymentCreateRequest` | `PaymentResponse` | `201` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Booking not found), 400 (amount must be > 0; Invalid timestamp) | Create resource (create). | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:47` (`create`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/close` | `propertyId:UUID, bookingId:UUID` | `-` | `RazorpayPaymentRequestCloseRequest` | `RazorpayPaymentRequestCloseResponse` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Provide exactly one of qrId or paymentLinkId; Razorpay settings not configured), 502 (Razorpay close request failed; Razorpay cancel request failed) | Close request. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:93` (`closeRequest`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/payment-link` | `propertyId:UUID, bookingId:UUID` | `-` | `RazorpayPaymentLinkCreateRequest` | `RazorpayPaymentLinkCreateResponse` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Booking is not active; Razorpay settings not configured), 502 (Razorpay request failed) | Create resource (create payment link). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentLinksController.kt:47` (`createPaymentLink`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | List resources (list qr). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:292` (`listQr`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr` | `propertyId:UUID, bookingId:UUID` | `-` | `RazorpayQrGenerateRequest` | `RazorpayQrGenerateResponse` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Booking is not active; Razorpay settings not configured), 502 (Razorpay request failed) | Create resource (create qr). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:56` (`createQr`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/active` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `RazorpayQrGenerateResponse?` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | Get resource (get active qr). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:157` (`getActiveQr`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/close` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `RazorpayQrGenerateResponse?` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Razorpay settings not configured; Razorpay test keys not configured), 502 (Razorpay close request failed) | Close active qr. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:180` (`closeActiveQr`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/close` | `propertyId:UUID, bookingId:UUID, qrId:String` | `-` | `-` | `RazorpayQrGenerateResponse?` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Razorpay settings not configured; Razorpay test keys not configured), 502 (Razorpay close request failed) | Close qr by id. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:212` (`closeQrById`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events` | `propertyId:UUID, qrId:String` | `-` | `-` | `List` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted) | Qr events. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:244` (`qrEvents`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr/{qrId}/events/stream` | `propertyId:UUID, bookingId:UUID, qrId:String` | `-` | `-` | `SseEmitter` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted) | Stream events/data (stream qr events). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayQrPayments.kt:273` (`streamQrEvents`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/refund` | `propertyId:UUID, bookingId:UUID` | `-` | `RazorpayRefundRequest` | `RazorpayRefundResponse` | `200` | Roles: ADMIN, FINANCE, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (paymentId is required; amount must be <= payment amount), 502 (Razorpay refund request failed) | Refund. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayRefundsController.kt:42` (`refund`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/payments/razorpay/requests` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | List resources (list requests). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayPaymentRequestsController.kt:45` (`listRequests`) | -| `DELETE` | `/properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}` | `propertyId:UUID, bookingId:UUID, paymentId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (Cash payments can only be deleted for OPEN or CHECKED_IN bookings; Only CASH payments can be deleted) | Delete resource (delete). | `src/main/kotlin/com/android/trisolarisserver/controller/payment/Payments.kt:103` (`delete`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/room-requests` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:103` (`list`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/room-requests` | `propertyId:UUID, bookingId:UUID` | `-` | `BookingRoomRequestCreateRequest` | `BookingRoomRequestResponse` | `201` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 400 (quantity must be > 0; fromAt required), 409 (Booking closed; Insufficient room type availability) | Create resource (create). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:48` (`create`) | -| `DELETE` | `/properties/{propertyId}/bookings/{bookingId}/room-requests/{requestId}` | `propertyId:UUID, bookingId:UUID, requestId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property), 409 (Cannot cancel fulfilled room request) | Cancel flow (cancel). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingRoomRequests.kt:121` (`cancel`) | -| `POST` | `/properties/{propertyId}/bookings/{bookingId}/room-stays/{roomStayId}/check-out` | `propertyId:UUID, bookingId:UUID, roomStayId:UUID` | `-` | `BookingCheckOutRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER, STAFF | 401 (User not found; Missing principal), 403 (Required property role not granted), 404 (Room stay not found for booking; Booking not found), 400 (Invalid timestamp), 409 (Booking not checked in; Room stay amount is outside allowed range) | Check out flow (check out room stay). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:583` (`checkOutRoomStay`) | -| `GET` | `/properties/{propertyId}/bookings/{bookingId}/stream` | `propertyId:UUID, bookingId:UUID` | `-` | `-` | `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` | `200` | Roles: ADMIN, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Required property role not granted), 404 (Booking not found; Booking not found for property) | Stream events/data (stream booking). | `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt:302` (`streamBooking`) | -| `GET` | `/properties/{propertyId}/cancellation-policy` | `propertyId:UUID` | `-` | `-` | `CancellationPolicyResponse` | `200` | Authenticated user (Firebase) | - | Get resource (get). | `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:34` (`get`) | -| `PUT` | `/properties/{propertyId}/cancellation-policy` | `propertyId:UUID` | `-` | `CancellationPolicyUpsertRequest` | `CancellationPolicyResponse` | `200` | Roles: ADMIN | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (freeDaysBeforeCheckin must be >= 0; Unknown penaltyMode) | Upsert. | `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:53` (`upsert`) | -| `GET` | `/properties/{propertyId}/code` | `propertyId:UUID` | `-` | `-` | `PropertyCodeResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found) | Get resource (get property code). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:105` (`getPropertyCode`) | -| `GET` | `/properties/{propertyId}/guests/search` | `propertyId:UUID` | `phone:String? (optional), vehicleNumber:String? (optional)` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (phone or vehicleNumber required) | Search. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:75` (`search`) | -| `GET` | `/properties/{propertyId}/guests/visit-count` | `propertyId:UUID` | `phone:String (required)` | `-` | `GuestVisitCountResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (phone required) | Get resource (get visit count). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:113` (`getVisitCount`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `GuestResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property) | Get resource (get guest). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:102` (`getGuest`) | -| `PUT` | `/properties/{propertyId}/guests/{guestId}` | `propertyId:UUID, guestId:UUID` | `-` | `GuestUpdateRequest` | `GuestResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property), 409 (Phone number already exists) | Update resource (update guest). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:45` (`updateGuest`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}/documents` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted) | List resources (list documents). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:125` (`listDocuments`) | -| `POST` | `/properties/{propertyId}/guests/{guestId}/documents` | `propertyId:UUID, guestId:UUID` | `bookingId:UUID (required)` | `-` | `GuestDocumentResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal; User not found), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (File is empty; Video files are not allowed), 409 (Duplicate document) | Upload document. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:65` (`uploadDocument`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}/documents/stream` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `org.springframework.web.servlet.mvc.method.annotation.SseEmitter` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted) | Stream events/data (stream documents). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:138` (`streamDocuments`) | -| `DELETE` | `/properties/{propertyId}/guests/{guestId}/documents/{documentId}` | `propertyId:UUID, guestId:UUID, documentId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Document not found), 400 (Documents can only be deleted for OPEN or CHECKED_IN bookings), 500 (Failed to delete file) | Delete resource (delete document). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:184` (`deleteDocument`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}/documents/{documentId}/file` | `propertyId:UUID, guestId:UUID, documentId:UUID` | `token:String? (optional)` | `-` | `ResponseEntity` | `200` | Roles: ADMIN, MANAGER | 401 (Invalid token; Missing principal), 403 (Required property role not granted), 404 (Document not found; File missing) | Download document. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestDocuments.kt:152` (`downloadDocument`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}/ratings` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property or guest not found), 400 (Guest not in property) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:79` (`list`) | -| `POST` | `/properties/{propertyId}/guests/{guestId}/ratings` | `propertyId:UUID, guestId:UUID` | `-` | `GuestRatingCreateRequest` | `GuestRatingResponse` | `201` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (Booking not in property; Booking not linked to guest), 409 (Rating already exists for booking) | Create resource (create). | `src/main/kotlin/com/android/trisolarisserver/controller/guest/GuestRatings.kt:42` (`create`) | -| `POST` | `/properties/{propertyId}/guests/{guestId}/signature` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `GuestResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Property or guest not found), 400 (File is empty; Only SVG allowed) | Upload signature. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:170` (`uploadSignature`) | -| `GET` | `/properties/{propertyId}/guests/{guestId}/signature/file` | `propertyId:UUID, guestId:UUID` | `-` | `-` | `ResponseEntity` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Signature not found; Property or guest not found), 400 (Guest not in property) | Download signature. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:194` (`downloadSignature`) | -| `POST` | `/properties/{propertyId}/guests/{guestId}/vehicles` | `propertyId:UUID, guestId:UUID` | `-` | `GuestVehicleRequest` | `GuestResponse` | `201` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Booking not found; Property or guest not found), 400 (Booking not in property; Guest not in property), 409 (Booking linked to different guest; Vehicle number already exists) | Add vehicle. | `src/main/kotlin/com/android/trisolarisserver/controller/guest/Guests.kt:130` (`addVehicle`) | -| `POST` | `/properties/{propertyId}/inbound-emails/manual` | `propertyId:UUID` | `file:MultipartFile (required)` | `-` | `ManualInboundResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (File is empty; Only PDF is supported) | Upload manual pdf. | `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmailManual.kt:41` (`uploadManualPdf`) | -| `GET` | `/properties/{propertyId}/inbound-emails/{emailId}/file` | `propertyId:UUID, emailId:UUID` | `-` | `-` | `ResponseEntity` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Email not found; Email PDF missing) | Download email pdf. | `src/main/kotlin/com/android/trisolarisserver/controller/email/InboundEmails.kt:32` (`downloadEmailPdf`) | -| `GET` | `/properties/{propertyId}/rate-plans` | `propertyId:UUID` | `roomTypeCode:String? (optional)` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:79` (`list`) | -| `POST` | `/properties/{propertyId}/rate-plans` | `propertyId:UUID` | `-` | `RatePlanCreateRequest` | `RatePlanResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Room type not found), 409 (Rate plan code already exists for room type) | Create resource (create). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:51` (`create`) | -| `DELETE` | `/properties/{propertyId}/rate-plans/{ratePlanId}` | `propertyId:UUID, ratePlanId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found) | Delete resource (delete). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:114` (`delete`) | -| `PUT` | `/properties/{propertyId}/rate-plans/{ratePlanId}` | `propertyId:UUID, ratePlanId:UUID` | `-` | `RatePlanUpdateRequest` | `RatePlanResponse` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found) | Update resource (update). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:95` (`update`) | -| `GET` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar` | `propertyId:UUID, ratePlanId:UUID` | `from:String (required), to:String (required)` | `-` | `RateCalendarAverageResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Rate plan not found), 400 (to must be on/after from; Invalid date format) | List resources (list calendar). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:170` (`listCalendar`) | -| `POST` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar` | `propertyId:UUID, ratePlanId:UUID` | `-` | `RateCalendarRangeUpsertRequest` | `List` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found), 400 (to must be on/after from; Invalid date format) | Upsert calendar. | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:129` (`upsertCalendar`) | -| `DELETE` | `/properties/{propertyId}/rate-plans/{ratePlanId}/calendar/{rateDate}` | `propertyId:UUID, ratePlanId:UUID, rateDate:String` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Rate plan not found), 400 (Invalid date format) | Delete resource (delete calendar). | `src/main/kotlin/com/android/trisolarisserver/controller/rate/RatePlans.kt:206` (`deleteCalendar`) | -| `GET` | `/properties/{propertyId}/razorpay-settings` | `propertyId:UUID` | `-` | `-` | `RazorpaySettingsResponse` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted) | Get resource (get settings). | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:33` (`getSettings`) | -| `PUT` | `/properties/{propertyId}/razorpay-settings` | `propertyId:UUID` | `-` | `RazorpaySettingsUpsertRequest` | `RazorpaySettingsResponse` | `200` | Roles: ADMIN | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found), 400 (keyId and keySecret must be provided together; keyIdTest and keySecretTest must be provided together) | Upsert settings. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:57` (`upsertSettings`) | -| `POST` | `/properties/{propertyId}/razorpay/return/failure` | `propertyId:UUID` | `-` | `-` | `Unit` | `204` | Public/unspecified | - | Failure. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:22` (`failure`) | -| `POST` | `/properties/{propertyId}/razorpay/return/success` | `propertyId:UUID` | `-` | `-` | `Unit` | `204` | Public/unspecified | - | Success. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:16` (`success`) | -| `POST` | `/properties/{propertyId}/razorpay/webhook` | `propertyId:UUID` | `-` | `String?` | `Unit` | `204` | Public/unspecified | 401 (Missing signature; Invalid signature), 404 (Property not found), 400 (Razorpay settings not configured; Webhook secret not configured) | Capture. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayWebhookCapture.kt:53` (`capture`) | -| `GET` | `/properties/{propertyId}/room-stays/active` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR | 401 (Missing principal), 403 (Agents cannot view active stays; Property membership required) | List resources (list active room stays). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:42` (`listActiveRoomStays`) | -| `GET` | `/properties/{propertyId}/room-stays/cards/{cardIndex}` | `propertyId:UUID, cardIndex:Int` | `-` | `-` | `IssuedCardResponse` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Card not found) | Get resource (get card by index). | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:158` (`getCardByIndex`) | -| `POST` | `/properties/{propertyId}/room-stays/cards/{cardIndex}/revoke` | `propertyId:UUID, cardIndex:Int` | `-` | `-` | `CardRevokeResponse` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Card not found) | Revoke. | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:138` (`revoke`) | -| `GET` | `/properties/{propertyId}/room-stays/{roomStayId}/cards` | `propertyId:UUID, roomStayId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, MANAGER, STAFF, SUPERVISOR | 401 (Missing principal), 404 (Room stay not found for property) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:125` (`list`) | -| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/cards` | `propertyId:UUID, roomStayId:UUID` | `-` | `IssueCardRequest` | `IssuedCardResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room stay not found for property), 400 (cardId required; cardIndex required), 409 (Active card already exists for room stay; Active card already exists for room) | Issue. | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:83` (`issue`) | -| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/cards/prepare` | `propertyId:UUID, roomStayId:UUID` | `-` | `CardPrepareRequest` | `CardPrepareResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room stay not found for property; Property not found), 400 (expiresAt required; expiresAt must be after issuedAt), 409 (Room stay is already closed) | Prepare. | `src/main/kotlin/com/android/trisolarisserver/controller/card/IssuedCards.kt:50` (`prepare`) | -| `POST` | `/properties/{propertyId}/room-stays/{roomStayId}/void` | `propertyId:UUID, roomStayId:UUID` | `-` | `RoomStayVoidRequest` | `Unit` | `200` | Roles: ADMIN, MANAGER, STAFF | 401 (Missing principal), 403 (Missing role; Cannot void stay after first payment), 404 (Room stay not found for property), 409 (Cannot void checked-out room stay) | Void room stay. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:78` (`voidRoomStay`) | -| `GET` | `/properties/{propertyId}/room-types` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required) | List resources (list room types). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:49` (`listRoomTypes`) | -| `POST` | `/properties/{propertyId}/room-types` | `propertyId:UUID` | `-` | `RoomTypeUpsertRequest` | `RoomTypeResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Property not found; Amenity not found), 409 (Room type code already exists for property) | Create resource (create room type). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:107` (`createRoomType`) | -| `GET` | `/properties/{propertyId}/room-types/{roomTypeCode}/images` | `propertyId:UUID, roomTypeCode:String` | `-` | `-` | `List` | `200` | Public/unspecified | 404 (Room type not found) | List resources (list by room type). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypeImages.kt:29` (`listByRoomType`) | -| `GET` | `/properties/{propertyId}/room-types/{roomTypeCode}/rate` | `propertyId:UUID, roomTypeCode:String` | `date:String (required), ratePlanCode:String? (optional)` | `-` | `RateResolveResponse` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found; Room type not found), 400 (Rate plan not for room type; Invalid date format) | Resolve rate. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:60` (`resolveRate`) | -| `DELETE` | `/properties/{propertyId}/room-types/{roomTypeId}` | `propertyId:UUID, roomTypeId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Room type not found) | Delete resource (delete room type). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:189` (`deleteRoomType`) | -| `PUT` | `/properties/{propertyId}/room-types/{roomTypeId}` | `propertyId:UUID, roomTypeId:UUID` | `-` | `RoomTypeUpsertRequest` | `RoomTypeResponse` | `200` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Room type not found; Amenity not found), 409 (Room type code already exists for property) | Update resource (update room type). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomTypes.kt:153` (`updateRoomType`) | -| `GET` | `/properties/{propertyId}/rooms` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Property membership required) | List resources (list rooms). | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:66` (`listRooms`) | -| `POST` | `/properties/{propertyId}/rooms` | `propertyId:UUID` | `-` | `RoomUpsertRequest` | `RoomResponse` | `201` | Roles: ADMIN | 401 (Missing principal), 403 (Required property role not granted), 404 (Property not found; Room type not found), 400 (roomTypeCode required), 409 (Room number already exists for property) | Create resource (create room). | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:278` (`createRoom`) | -| `GET` | `/properties/{propertyId}/rooms/availability` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required) | Room availability. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:126` (`roomAvailability`) | -| `GET` | `/properties/{propertyId}/rooms/availability-range` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Invalid date range; Invalid date format) | Room availability range. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:184` (`roomAvailabilityRange`) | -| `GET` | `/properties/{propertyId}/rooms/available` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Authenticated user (Firebase) | - | Available rooms. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:147` (`availableRooms`) | -| `GET` | `/properties/{propertyId}/rooms/available-range-with-rate` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Property not found), 400 (Invalid date range; Invalid date format) | Available rooms with rate. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:221` (`availableRoomsWithRate`) | -| `GET` | `/properties/{propertyId}/rooms/board` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 401 (Missing principal), 403 (Property membership required) | Room board. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:86` (`roomBoard`) | -| `GET` | `/properties/{propertyId}/rooms/board/stream` | `propertyId:UUID` | `-` | `-` | `SseEmitter` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required) | Room board stream. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:114` (`roomBoardStream`) | -| `GET` | `/properties/{propertyId}/rooms/by-type/{roomTypeCode}` | `propertyId:UUID, roomTypeCode:String` | `-` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, HOUSEKEEPING, MANAGER, STAFF | 404 (Room type not found) | Rooms by type. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:160` (`roomsByType`) | -| `DELETE` | `/properties/{propertyId}/rooms/{roomId}` | `propertyId:UUID, roomId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Property membership required), 404 (Room not found for property), 409 (Cannot delete room with stays), 500 (Failed to delete room image files) | Delete resource (delete room). | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:367` (`deleteRoom`) | -| `PUT` | `/properties/{propertyId}/rooms/{roomId}` | `propertyId:UUID, roomId:UUID` | `-` | `RoomUpsertRequest` | `RoomResponse` | `200` | Roles: ADMIN | 401 (Missing principal), 403 (Required property role not granted), 404 (Room not found for property; Room type not found), 400 (roomTypeCode required), 409 (Room number already exists for property) | Update resource (update room). | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:323` (`updateRoom`) | -| `POST` | `/properties/{propertyId}/rooms/{roomId}/cards/prepare-temp` | `propertyId:UUID, roomId:UUID` | `-` | `-` | `CardPrepareResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room not found; Property not found) | Prepare temporary. | `src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:46` (`prepareTemporary`) | -| `POST` | `/properties/{propertyId}/rooms/{roomId}/cards/temp` | `propertyId:UUID, roomId:UUID` | `-` | `IssueTempCardRequest` | `IssuedCardResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal; User not found), 403 (Property membership required), 404 (Room not found), 400 (cardId required; cardIndex required), 409 (Active card already exists for room) | Issue temporary. | `src/main/kotlin/com/android/trisolarisserver/controller/card/TemporaryRoomCards.kt:74` (`issueTemporary`) | -| `GET` | `/properties/{propertyId}/rooms/{roomId}/images` | `propertyId:UUID, roomId:UUID` | `-` | `-` | `List` | `200` | Authenticated user (Firebase) | 404 (Room not found) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:55` (`list`) | -| `POST` | `/properties/{propertyId}/rooms/{roomId}/images` | `propertyId:UUID, roomId:UUID` | `file:MultipartFile (required), tagIds:List? (optional)` | `-` | `RoomImageResponse` | `201` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Tag not found; Room not found), 400 (File is empty), 409 (Duplicate image for room) | Upload. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:85` (`upload`) | -| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/reorder-room` | `propertyId:UUID, roomId:UUID` | `-` | `RoomImageReorderRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 400 (Images do not belong to room) | Reorder room images. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:199` (`reorderRoomImages`) | -| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/reorder-room-type` | `propertyId:UUID, roomId:UUID` | `-` | `RoomImageReorderRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 400 (Images do not belong to room type; Images do not belong to property) | Reorder room type images. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:227` (`reorderRoomTypeImages`) | -| `DELETE` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}` | `propertyId:UUID, roomId:UUID, imageId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Room not found), 500 (Failed to delete image files) | Delete resource (delete). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:129` (`delete`) | -| `GET` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}/file` | `propertyId:UUID, roomId:UUID, imageId:UUID` | `size:String (optional)` | `-` | `ResponseEntity` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required), 404 (Image not found; File missing) | File. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:257` (`file`) | -| `PUT` | `/properties/{propertyId}/rooms/{roomId}/images/{imageId}/tags` | `propertyId:UUID, roomId:UUID, imageId:UUID` | `-` | `RoomImageTagUpdateRequest` | `Unit` | `204` | Roles: ADMIN, MANAGER | 401 (Missing principal), 403 (Required property role not granted), 404 (Image not found; Tag not found) | Update resource (update tags). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImages.kt:180` (`updateTags`) | -| `GET` | `/properties/{propertyId}/transport-modes` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Any property member | 401 (Missing principal), 403 (Property membership required) | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:26` (`list`) | -| `GET` | `/properties/{propertyId}/users` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR | 401 (Missing principal), 403 (Property membership required) | List resources (list property users). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:165` (`listPropertyUsers`) | -| `GET` | `/properties/{propertyId}/users/search` | `propertyId:UUID` | `phone:String? (optional)` | `-` | `List` | `200` | Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR | 401 (Missing principal), 403 (Property membership required) | Search property users. | `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:50` (`searchPropertyUsers`) | -| `DELETE` | `/properties/{propertyId}/users/{userId}` | `propertyId:UUID, userId:UUID` | `-` | `-` | `Unit` | `204` | Roles: ADMIN | 401 (Missing principal), 403 (Property membership required) | Delete resource (delete property user). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:290` (`deletePropertyUser`) | -| `PUT` | `/properties/{propertyId}/users/{userId}/disabled` | `propertyId:UUID, userId:UUID` | `-` | `PropertyUserDisableRequest` | `PropertyUserResponse` | `200` | Roles: ADMIN, AGENT, FINANCE, GUIDE, HOUSEKEEPING, MANAGER, STAFF, SUPERVISOR | 401 (Missing principal), 403 (Role not allowed; Property membership required), 404 (User not found in property) | Update resource (update property user disabled). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:240` (`updatePropertyUserDisabled`) | -| `PUT` | `/properties/{propertyId}/users/{userId}/roles` | `propertyId:UUID, userId:UUID` | `-` | `PropertyUserRoleRequest` | `PropertyUserResponse` | `200` | Roles: ADMIN, AGENT, MANAGER, STAFF | 401 (Missing principal), 403 (Missing role; Role not allowed), 404 (Property not found; User not found), 400 (Unknown role) | Upsert property user roles. | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:189` (`upsertPropertyUserRoles`) | -| `GET` | `/users` | `-` | `phone:String? (optional)` | `-` | `List` | `200` | SUPER_ADMIN | 401 (User not found), 403 (Super admin only) | List resources (list app users). | `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:27` (`listAppUsers`) | diff --git a/scripts/generate_api_docs.py b/scripts/generate_api_docs.py deleted file mode 100644 index efaac10..0000000 --- a/scripts/generate_api_docs.py +++ /dev/null @@ -1,888 +0,0 @@ -#!/usr/bin/env python3 -"""Generate API docs from Spring controller source.""" - -from __future__ import annotations - -import glob -import os -import re -from dataclasses import dataclass, field -from pathlib import Path -from typing import Dict, Iterable, List, Sequence, Set, Tuple - - -ROOT = Path(__file__).resolve().parents[1] -CONTROLLER_ROOT = ROOT / "src/main/kotlin/com/android/trisolarisserver/controller" -REFERENCE_OUTPUT = ROOT / "docs/API_REFERENCE.md" -CATALOG_OUTPUT = ROOT / "docs/API_CATALOG.md" - -HTTP_BY_ANNOTATION = { - "GetMapping": "GET", - "PostMapping": "POST", - "PutMapping": "PUT", - "DeleteMapping": "DELETE", - "PatchMapping": "PATCH", -} - -HTTP_STATUS_CODE = { - "CONTINUE": "100", - "SWITCHING_PROTOCOLS": "101", - "OK": "200", - "CREATED": "201", - "ACCEPTED": "202", - "NO_CONTENT": "204", - "MOVED_PERMANENTLY": "301", - "FOUND": "302", - "BAD_REQUEST": "400", - "UNAUTHORIZED": "401", - "FORBIDDEN": "403", - "NOT_FOUND": "404", - "METHOD_NOT_ALLOWED": "405", - "CONFLICT": "409", - "UNPROCESSABLE_ENTITY": "422", - "TOO_MANY_REQUESTS": "429", - "INTERNAL_SERVER_ERROR": "500", - "BAD_GATEWAY": "502", - "SERVICE_UNAVAILABLE": "503", -} - -KNOWN_HELPER_ERRORS: Dict[str, List[Tuple[str, str]]] = { - "requirePrincipal": [("UNAUTHORIZED", "Missing principal")], - "requireUser": [("UNAUTHORIZED", "User not found")], - "requireMember": [ - ("UNAUTHORIZED", "Missing principal"), - ("FORBIDDEN", "Property membership required"), - ], - "requireRole": [ - ("UNAUTHORIZED", "Missing principal"), - ("FORBIDDEN", "Required property role not granted"), - ], - "requireSuperAdmin": [ - ("UNAUTHORIZED", "User not found"), - ("FORBIDDEN", "Super admin only"), - ], - "requireProperty": [("NOT_FOUND", "Property not found")], - "requirePropertyGuest": [ - ("NOT_FOUND", "Property or guest not found"), - ("BAD_REQUEST", "Guest not in property"), - ], - "requireRoomStayForProperty": [("NOT_FOUND", "Room stay not found for property")], - "requireOpenRoomStayForProperty": [ - ("NOT_FOUND", "Room stay not found for property"), - ("CONFLICT", "Room stay is already closed"), - ], - "parseOffset": [("BAD_REQUEST", "Invalid timestamp")], - "parseDate": [("BAD_REQUEST", "Invalid date format")], -} - -KNOWN_SIDE_EFFECTS = ( - ("bookingEvents.emit", "Emits booking SSE updates."), - ("roomBoardEvents.emit", "Emits room board SSE updates."), - ("roomBoardEvents.emitRoom", "Emits room board SSE updates."), - ("subscribe(", "Streams SSE events."), - ("logStayAudit(", "Writes room-stay audit log."), - ("roomStayAuditLogRepo.save", "Writes room-stay audit log."), - ("guestDocumentRepo.save", "Stores/updates guest document metadata."), - ("guestDocumentRepo.delete", "Deletes guest document metadata."), - ("storageService.store", "Stores file payload on configured storage."), - ("storageService.delete", "Deletes file payload from configured storage."), -) - -KEYWORDS = { - "if", - "for", - "while", - "when", - "return", - "throw", - "catch", - "try", - "else", - "do", - "super", - "this", - "listOf", - "mapOf", - "setOf", - "mutableListOf", - "mutableMapOf", - "mutableSetOf", - "arrayOf", - "require", - "check", - "println", -} - - -@dataclass -class DtoField: - name: str - type_name: str - optional: bool - - -@dataclass -class FunctionInfo: - name: str - line: int - annotations: List[str] - param_blob: str - response_type: str - body: str - calls: Set[str] - errors: List[Tuple[str, str]] - roles: Set[str] - has_principal: bool - - -@dataclass -class Endpoint: - method: str - path: str - path_params: List[str] - query_params: List[str] - body_type: str - response_type: str - status: str - behavior: str - handler_file: str - handler_name: str - handler_line: int - auth: str = "-" - validation_notes: List[str] = field(default_factory=list) - common_errors: List[Tuple[str, List[str]]] = field(default_factory=list) - side_effects: List[str] = field(default_factory=list) - body_shape: str = "-" - - -def split_params(param_blob: str) -> List[str]: - parts: List[str] = [] - buf: List[str] = [] - angle = 0 - paren = 0 - bracket = 0 - brace = 0 - for ch in param_blob: - if ch == "<": - angle += 1 - elif ch == ">": - angle = max(0, angle - 1) - elif ch == "(": - paren += 1 - elif ch == ")": - paren = max(0, paren - 1) - elif ch == "[": - bracket += 1 - elif ch == "]": - bracket = max(0, bracket - 1) - elif ch == "{": - brace += 1 - elif ch == "}": - brace = max(0, brace - 1) - if ch == "," and angle == 0 and paren == 0 and bracket == 0 and brace == 0: - part = "".join(buf).strip() - if part: - parts.append(part) - buf = [] - continue - buf.append(ch) - tail = "".join(buf).strip() - if tail: - parts.append(tail) - return parts - - -def normalize_space(value: str) -> str: - return " ".join(value.split()) - - -def http_code(value: str) -> str: - if value.isdigit(): - return value - return HTTP_STATUS_CODE.get(value, value) - - -def method_from_annotations(annotations: Sequence[str]) -> List[str]: - for ann in annotations: - m = re.match(r"@(\w+)", ann.strip()) - if not m: - continue - name = m.group(1) - if name in HTTP_BY_ANNOTATION: - return [HTTP_BY_ANNOTATION[name]] - if name == "RequestMapping": - methods = re.findall(r"RequestMethod\.(GET|POST|PUT|DELETE|PATCH)", ann) - return methods or ["ANY"] - return [] - - -def mapping_path(annotations: Sequence[str]) -> str: - for ann in annotations: - if "Mapping" not in ann: - continue - m = re.search(r'"([^"]*)"', ann) - if m: - return m.group(1) - return "" - - -def extract_types_from_params(param_blob: str) -> Tuple[List[str], List[str], str, bool]: - path_params: List[str] = [] - query_params: List[str] = [] - body_type = "-" - has_principal = False - for raw in split_params(param_blob): - segment = normalize_space(raw) - name_match = re.search(r"(\w+)\s*:", segment) - param_name = name_match.group(1) if name_match else "param" - type_match = re.search(r":\s*([^=]+)", segment) - param_type = type_match.group(1).strip() if type_match else "Unknown" - if "@AuthenticationPrincipal" in segment: - has_principal = True - if "@PathVariable" in segment: - path_params.append(f"{param_name}:{param_type}") - elif "@RequestParam" in segment: - required = "optional" if "required = false" in segment else "required" - query_params.append(f"{param_name}:{param_type} ({required})") - elif "@RequestBody" in segment: - body_type = param_type - return path_params, query_params, body_type, has_principal - - -def explicit_status_from_annotations(annotations: Sequence[str]) -> str | None: - for ann in annotations: - if not ann.strip().startswith("@ResponseStatus"): - continue - if "CREATED" in ann: - return "201" - if "NO_CONTENT" in ann: - return "204" - m = re.search(r"HttpStatus\.([A-Z_]+)", ann) - if m: - return http_code(m.group(1)) - return None - - -def default_status(method: str, response_type: str, explicit: str | None) -> str: - if explicit: - return explicit - if method == "DELETE" and response_type == "Unit": - return "204" - return "200" - - -def behavior_from_name(name: str) -> str: - words = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name).lower() - if name.startswith("list"): - return f"List resources ({words})." - if name.startswith("create"): - return f"Create resource ({words})." - if name.startswith("update"): - return f"Update resource ({words})." - if name.startswith("delete"): - return f"Delete resource ({words})." - if name.startswith("get"): - return f"Get resource ({words})." - if name.startswith("stream"): - return f"Stream events/data ({words})." - if name.startswith("checkOut"): - return f"Check out flow ({words})." - if name.startswith("checkIn"): - return f"Check in flow ({words})." - if name.startswith("cancel"): - return f"Cancel flow ({words})." - if name.startswith("noShow"): - return f"No-show flow ({words})." - return f"{words.capitalize()}." - - -def join_paths(base: str, rel: str) -> str: - if not base and not rel: - return "/" - parts = [p.strip("/") for p in (base, rel) if p] - return "/" + "/".join(parts) - - -def parse_return_type(tail: str) -> str: - m = re.search(r":\s*([^{=]+)", tail) - if not m: - return "Unit" - return m.group(1).strip() - - -def find_matching_paren(text: str, open_index: int) -> int: - depth = 0 - for idx in range(open_index, len(text)): - ch = text[idx] - if ch == "(": - depth += 1 - elif ch == ")": - depth -= 1 - if depth == 0: - return idx - return -1 - - -def extract_parenthesized(text: str, open_index: int) -> str: - close_index = find_matching_paren(text, open_index) - if close_index == -1: - return "" - return text[open_index + 1 : close_index] - - -def strip_leading_annotations(segment: str) -> str: - value = segment.strip() - while value.startswith("@"): - if "(" in value and (value.find("(") < value.find(" ") if " " in value else True): - open_index = value.find("(") - close_index = find_matching_paren(value, open_index) - if close_index == -1: - break - value = value[close_index + 1 :].strip() - else: - value = re.sub(r"^@\w+\s*", "", value).strip() - return value - - -def extract_calls(body: str) -> Set[str]: - calls = set() - for name in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body): - if name in KEYWORDS: - continue - calls.add(name) - return calls - - -def extract_errors(body: str) -> List[Tuple[str, str]]: - errors: List[Tuple[str, str]] = [] - pattern = re.compile( - r"ResponseStatusException\(\s*HttpStatus\.([A-Z_]+)\s*,\s*\"([^\"]+)\"", - re.DOTALL, - ) - for status, message in pattern.findall(body): - errors.append((status, normalize_space(message))) - return errors - - -def extract_roles(body: str) -> Set[str]: - return set(re.findall(r"Role\.([A-Z_]+)", body)) - - -def parse_function_signature(lines: List[str], start_line: int) -> Tuple[str, str, int, int, str]: - current_line = lines[start_line] - open_col = current_line.find("(") - if open_col == -1: - return "", "Unit", start_line, len(current_line), "" - - depth = 1 - param_chars: List[str] = [] - line_idx = start_line - close_col = open_col - while line_idx < len(lines): - line = lines[line_idx] - start = open_col + 1 if line_idx == start_line else 0 - col = start - while col < len(line): - ch = line[col] - if depth > 0: - if ch == "(": - depth += 1 - param_chars.append(ch) - elif ch == ")": - depth -= 1 - if depth == 0: - close_col = col - break - param_chars.append(ch) - else: - param_chars.append(ch) - col += 1 - if depth == 0: - break - line_idx += 1 - - tail_parts: List[str] = [] - if line_idx < len(lines): - tail_parts.append(lines[line_idx][close_col + 1 :]) - look = line_idx + 1 - while look < len(lines): - trimmed = lines[look].strip() - if not trimmed: - tail_parts.append(" ") - look += 1 - continue - if trimmed.startswith("@"): - break - if trimmed.startswith("fun ") or trimmed.startswith("private fun ") or trimmed.startswith("internal fun "): - break - tail_parts.append(" " + trimmed) - if "{" in trimmed or "=" in trimmed: - break - look += 1 - tail = "".join(tail_parts).strip() - return "".join(param_chars).strip(), parse_return_type(tail), line_idx, close_col, tail - - -def parse_function_body(lines: List[str], signature_end_line: int, signature_end_col: int) -> Tuple[str, int]: - line_idx = signature_end_line - col = signature_end_col + 1 - marker_line = -1 - marker_col = -1 - marker = "" - while line_idx < len(lines): - line = lines[line_idx] - start = col if line_idx == signature_end_line else 0 - for idx in range(start, len(line)): - ch = line[idx] - if ch == "{": - marker_line = line_idx - marker_col = idx - marker = "{" - break - if ch == "=": - marker_line = line_idx - marker_col = idx - marker = "=" - break - if marker: - break - line_idx += 1 - - if not marker: - return "", signature_end_line - if marker == "=": - expression = lines[marker_line][marker_col + 1 :].strip() - return expression, marker_line - - body_parts: List[str] = [] - depth = 1 - for li in range(marker_line, len(lines)): - line = lines[li] - start = marker_col + 1 if li == marker_line else 0 - segment_start = start - for cj in range(start, len(line)): - ch = line[cj] - if ch == "{": - if depth >= 1 and segment_start <= cj: - body_parts.append(line[segment_start:cj]) - depth += 1 - segment_start = cj + 1 - elif ch == "}": - if depth >= 1 and segment_start <= cj: - body_parts.append(line[segment_start:cj]) - depth -= 1 - if depth == 0: - return "".join(body_parts), li - segment_start = cj + 1 - if depth >= 1: - body_parts.append(line[segment_start:]) - body_parts.append("\n") - return "".join(body_parts), len(lines) - 1 - - -def parse_data_classes() -> Dict[str, List[DtoField]]: - dto_map: Dict[str, List[DtoField]] = {} - for file_name in sorted(glob.glob(str(CONTROLLER_ROOT / "**/*.kt"), recursive=True)): - text = Path(file_name).read_text(encoding="utf-8") - for match in re.finditer(r"\bdata class\s+(\w+)\s*\(", text): - class_name = match.group(1) - open_index = text.find("(", match.start()) - if open_index == -1: - continue - blob = extract_parenthesized(text, open_index) - fields: List[DtoField] = [] - for raw in split_params(blob): - cleaned = strip_leading_annotations(raw) - if not cleaned: - continue - field_match = re.search(r"(?:val|var)\s+(\w+)\s*:\s*([^=]+?)(?:\s*=\s*.+)?$", cleaned) - if not field_match: - continue - field_name = field_match.group(1) - type_name = field_match.group(2).strip() - optional = "?" in type_name or "=" in cleaned - fields.append(DtoField(field_name, type_name, optional)) - if fields: - dto_map[class_name] = fields - return dto_map - - -def parse_controller_file(file_path: Path) -> Tuple[List[Endpoint], Dict[str, FunctionInfo]]: - text = file_path.read_text(encoding="utf-8") - if "@RestController" not in text and "@Controller" not in text: - return [], {} - - lines = text.splitlines() - endpoints: List[Endpoint] = [] - functions: Dict[str, FunctionInfo] = {} - pending_annotations: List[str] = [] - current_base_path = "" - - i = 0 - while i < len(lines): - stripped = lines[i].strip() - if stripped.startswith("@"): - pending_annotations.append(stripped) - i += 1 - continue - - class_match = re.search(r"\bclass\b", stripped) - if class_match: - mapped_base = "" - for ann in pending_annotations: - if ann.startswith("@RequestMapping"): - mapped_base = mapping_path([ann]) - break - if mapped_base: - current_base_path = mapped_base - pending_annotations = [] - i += 1 - continue - - fun_match = re.search(r"\bfun\s+(\w+)\s*\(", stripped) - if fun_match: - name = fun_match.group(1) - function_annotations = pending_annotations[:] - pending_annotations = [] - - param_blob, return_type, sig_end_line, sig_end_col, _ = parse_function_signature(lines, i) - body, body_end_line = parse_function_body(lines, sig_end_line, sig_end_col) - path_params, query_params, body_type, has_principal = extract_types_from_params(param_blob) - direct_errors = extract_errors(body) - roles = extract_roles(body) - calls = extract_calls(body) - - functions[name] = FunctionInfo( - name=name, - line=i + 1, - annotations=function_annotations, - param_blob=param_blob, - response_type=return_type, - body=body, - calls=calls, - errors=direct_errors, - roles=roles, - has_principal=has_principal, - ) - - method_names = method_from_annotations(function_annotations) - if method_names: - rel_path = mapping_path(function_annotations) - full_path = join_paths(current_base_path, rel_path) - explicit_status = explicit_status_from_annotations(function_annotations) - behavior = behavior_from_name(name) - rel_file = os.path.relpath(file_path, ROOT) - for method in method_names: - endpoints.append( - Endpoint( - method=method, - path=full_path, - path_params=path_params, - query_params=query_params, - body_type=body_type, - response_type=return_type, - status=default_status(method, return_type, explicit_status), - behavior=behavior, - handler_file=rel_file, - handler_name=name, - handler_line=i + 1, - ) - ) - i = body_end_line + 1 - continue - - if stripped and not stripped.startswith("//"): - pending_annotations = [] - i += 1 - - return endpoints, functions - - -def collect_function_metadata( - function_name: str, - functions: Dict[str, FunctionInfo], - visiting: Set[str] | None = None, -) -> Tuple[List[Tuple[str, str]], Set[str], Set[str], Set[str]]: - if visiting is None: - visiting = set() - if function_name in visiting: - return [], set(), set(), set() - func = functions.get(function_name) - if not func: - return [], set(), set(), set() - visiting.add(function_name) - - errors = list(func.errors) - roles = set(func.roles) - calls = set(func.calls) - auth_hints: Set[str] = set() - if "requireSuperAdmin" in calls: - auth_hints.add("SUPER_ADMIN") - if "requireMember" in calls: - auth_hints.add("PROPERTY_MEMBER") - if "requireRole" in calls and roles: - auth_hints.add("ROLE_BASED") - if func.has_principal: - auth_hints.add("AUTHENTICATED") - - for call in list(calls): - if call in KNOWN_HELPER_ERRORS: - errors.extend(KNOWN_HELPER_ERRORS[call]) - if call in functions and call != function_name: - child_errors, child_roles, child_calls, child_auth_hints = collect_function_metadata( - call, functions, visiting - ) - errors.extend(child_errors) - roles.update(child_roles) - calls.update(child_calls) - auth_hints.update(child_auth_hints) - - return errors, roles, calls, auth_hints - - -def unique_messages(values: Iterable[str]) -> List[str]: - seen = set() - out: List[str] = [] - for value in values: - key = value.strip() - if not key or key in seen: - continue - seen.add(key) - out.append(key) - return out - - -def resolve_auth_label( - roles: Set[str], - calls: Set[str], - auth_hints: Set[str], - has_principal: bool, -) -> str: - if "SUPER_ADMIN" in auth_hints: - return "SUPER_ADMIN" - if roles: - return "Roles: " + ", ".join(sorted(roles)) - if "PROPERTY_MEMBER" in auth_hints: - return "Any property member" - if has_principal or "AUTHENTICATED" in auth_hints: - return "Authenticated user (Firebase)" - return "Public/unspecified" - - -def summarize_errors(errors: Sequence[Tuple[str, str]]) -> List[Tuple[str, List[str]]]: - grouped: Dict[str, List[str]] = {} - for status, message in errors: - code = http_code(status) - grouped.setdefault(code, []).append(message) - ordering = ["401", "403", "404", "400", "409", "422", "429", "500"] - ordered_codes = sorted(grouped.keys(), key=lambda code: (ordering.index(code) if code in ordering else 999, code)) - summary: List[Tuple[str, List[str]]] = [] - for code in ordered_codes: - messages = unique_messages(grouped[code])[:4] - summary.append((code, messages)) - return summary - - -def resolve_body_shape(body_type: str, dto_fields: Dict[str, List[DtoField]]) -> str: - if body_type == "-": - return "-" - clean = body_type.strip().replace("?", "") - candidates = [clean] - if "." in clean: - candidates.append(clean.split(".")[-1]) - if "<" in clean and ">" in clean: - inner = clean[clean.find("<") + 1 : clean.rfind(">")].strip() - candidates.append(inner) - if "." in inner: - candidates.append(inner.split(".")[-1]) - - for candidate in candidates: - fields = dto_fields.get(candidate) - if not fields: - continue - chunks = [] - for field in fields: - label = f"{field.name}:{field.type_name}" - if field.optional: - label += " (optional)" - chunks.append(label) - joined = ", ".join(chunks) - return f"{candidate} {{ {joined} }}" - return body_type - - -def resolve_side_effects(body: str, response_type: str) -> List[str]: - effects = [] - if "SseEmitter" in response_type: - effects.append("Streams SSE events.") - for pattern, description in KNOWN_SIDE_EFFECTS: - if pattern in body: - effects.append(description) - return unique_messages(effects) - - -def enrich_endpoints( - endpoints: List[Endpoint], - file_function_map: Dict[str, Dict[str, FunctionInfo]], - dto_fields: Dict[str, List[DtoField]], -) -> List[Endpoint]: - enriched: List[Endpoint] = [] - for endpoint in endpoints: - functions = file_function_map.get(endpoint.handler_file, {}) - function_info = functions.get(endpoint.handler_name) - if function_info is None: - enriched.append(endpoint) - continue - - errors, roles, calls, auth_hints = collect_function_metadata(endpoint.handler_name, functions) - error_summary = summarize_errors(errors) - validations = [] - for code, messages in error_summary: - if code not in {"400", "409", "422"}: - continue - for message in messages: - validations.append(f"{code}: {message}") - - endpoint.auth = resolve_auth_label(roles, calls, auth_hints, function_info.has_principal) - endpoint.common_errors = error_summary - endpoint.validation_notes = validations - endpoint.body_shape = resolve_body_shape(endpoint.body_type, dto_fields) - endpoint.side_effects = resolve_side_effects(function_info.body, endpoint.response_type) - enriched.append(endpoint) - return enriched - - -def write_reference(endpoints: Sequence[Endpoint]) -> None: - lines = [ - "# API Reference", - "", - "Generated from controller source.", - "", - f"- Total endpoints: **{len(endpoints)}**", - "- Auth: Firebase Bearer token unless endpoint is public.", - "- Regenerate: `python scripts/generate_api_docs.py`", - "", - "## Usage Template", - "", - "```bash", - "curl -X \"https://api.hoteltrisolaris.in\" \\", - " -H \"Authorization: Bearer \" \\", - " -H \"Content-Type: application/json\" \\", - " -d ''", - "```", - "", - "| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Auth | Common Errors | Behavior | Handler |", - "|---|---|---|---|---|---|---|---|---|---|---|", - ] - - for endpoint in endpoints: - path_params = ", ".join(endpoint.path_params) if endpoint.path_params else "-" - query_params = ", ".join(endpoint.query_params) if endpoint.query_params else "-" - handler = f"`{endpoint.handler_file}:{endpoint.handler_line}` (`{endpoint.handler_name}`)" - if endpoint.common_errors: - parts = [] - for code, messages in endpoint.common_errors: - if messages: - parts.append(f"{code} ({'; '.join(messages[:2])})") - else: - parts.append(code) - errors_text = ", ".join(parts) - else: - errors_text = "-" - lines.append( - f"| `{endpoint.method}` | `{endpoint.path}` | `{path_params}` | `{query_params}` | `{endpoint.body_type}` | `{endpoint.response_type}` | `{endpoint.status}` | {endpoint.auth} | {errors_text} | {endpoint.behavior} | {handler} |" - ) - - REFERENCE_OUTPUT.parent.mkdir(parents=True, exist_ok=True) - REFERENCE_OUTPUT.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def write_catalog(endpoints: Sequence[Endpoint]) -> None: - grouped: Dict[str, List[Endpoint]] = {} - for endpoint in endpoints: - grouped.setdefault(endpoint.handler_file, []).append(endpoint) - - lines = [ - "# API Catalog", - "", - "Behavior-first catalog generated from controller source.", - "", - f"- Total endpoints: **{len(endpoints)}**", - "- Notes: validations/errors are extracted from explicit `ResponseStatusException` checks and shared helper guards.", - "- Regenerate: `python scripts/generate_api_docs.py`", - "", - ] - - for handler_file in sorted(grouped.keys()): - entries = sorted(grouped[handler_file], key=lambda e: (e.path, e.method, e.handler_name)) - lines.append(f"## `{handler_file}`") - lines.append("") - for endpoint in entries: - lines.append(f"### `{endpoint.method} {endpoint.path}`") - lines.append("") - lines.append(f"- Handler: `{endpoint.handler_name}` (`{endpoint.handler_file}:{endpoint.handler_line}`)") - lines.append(f"- Behavior: {endpoint.behavior}") - if endpoint.path_params: - lines.append(f"- Path params: {', '.join(endpoint.path_params)}") - if endpoint.query_params: - lines.append(f"- Query params: {', '.join(endpoint.query_params)}") - if endpoint.body_type == "-": - lines.append("- Body: none") - else: - lines.append(f"- Body: {endpoint.body_shape}") - if endpoint.side_effects: - lines.append(f"- Side effects: {' '.join(endpoint.side_effects)}") - lines.append(f"- Auth: {endpoint.auth}") - lines.append(f"- Response: `{endpoint.status}` `{endpoint.response_type}`") - if endpoint.validation_notes: - lines.append("- Validation/guard checks:") - for note in endpoint.validation_notes: - lines.append(f" - {note}") - if endpoint.common_errors: - summary = [] - for code, messages in endpoint.common_errors: - if messages: - summary.append(f"{code} ({'; '.join(messages[:2])})") - else: - summary.append(code) - lines.append(f"- Common errors: {', '.join(summary)}") - else: - lines.append("- Common errors: none observed in controller checks") - lines.append("") - - CATALOG_OUTPUT.parent.mkdir(parents=True, exist_ok=True) - CATALOG_OUTPUT.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") - - -def main() -> None: - dto_fields = parse_data_classes() - all_endpoints: List[Endpoint] = [] - file_function_map: Dict[str, Dict[str, FunctionInfo]] = {} - - for file_name in sorted(glob.glob(str(CONTROLLER_ROOT / "**/*.kt"), recursive=True)): - file_path = Path(file_name) - endpoints, functions = parse_controller_file(file_path) - rel_file = os.path.relpath(file_path, ROOT) - if endpoints: - all_endpoints.extend(endpoints) - if functions: - file_function_map[rel_file] = functions - - uniq: Dict[Tuple[str, str, str, str], Endpoint] = {} - for endpoint in all_endpoints: - key = (endpoint.method, endpoint.path, endpoint.handler_file, endpoint.handler_name) - uniq[key] = endpoint - - ordered = sorted( - enrich_endpoints(list(uniq.values()), file_function_map, dto_fields), - key=lambda e: (e.path, e.method, e.handler_file, e.handler_name), - ) - write_reference(ordered) - write_catalog(ordered) - print( - f"Wrote {REFERENCE_OUTPUT} and {CATALOG_OUTPUT} ({len(ordered)} endpoints)" - ) - - -if __name__ == "__main__": - main()