diff --git a/docs/API_CATALOG.md b/docs/API_CATALOG.md index fc59962..d81a07e 100644 --- a/docs/API_CATALOG.md +++ b/docs/API_CATALOG.md @@ -1,196 +1,1575 @@ # API Catalog -Source of truth for implemented HTTP endpoints. Generated from controller annotations in `src/main/kotlin/com/android/trisolarisserver/controller`. +Behavior-first catalog generated from controller source. - Total endpoints: **125** -- Columns: method, path, handler (controller function). +- Notes: validations/errors are extracted from explicit `ResponseStatusException` checks and shared helper guards. +- Regenerate: `python scripts/generate_api_docs.py` -## System +## `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt` -| 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`) | +### `GET /icons/png` -## Auth +- 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 -| 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`) | +### `GET /icons/png/{filename}` -## Properties & Users +- 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 -| 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`) | +## `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt` -## Bookings +### `GET /auth/me` -| 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`) | +- 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 -## Guests & Documents +### `PUT /auth/me` -| 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`) | +- 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) -## Rooms & Images +### `POST /auth/verify` -| 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`) | +- 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 -## Room Types +## `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingBalances.kt` -| 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`) | +### `GET /properties/{propertyId}/bookings/{bookingId}/balance` -## Room Stays & Cards +- 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) -| 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`) | +## `src/main/kotlin/com/android/trisolarisserver/controller/booking/BookingFlow.kt` -## Rate Plans +### `GET /properties/{propertyId}/bookings` -| 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`) | +- 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) -## Cancellation Policy +### `POST /properties/{propertyId}/bookings` -| 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`) | +- 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) -## Transport Modes +### `GET /properties/{propertyId}/bookings/{bookingId}` -| Method | Path | Handler | -|---|---|---| -| `GET` | `/properties/{propertyId}/transport-modes` | `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:25` (`list`) | +- 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) -## Inbound Email +### `POST /properties/{propertyId}/bookings/{bookingId}/billing-policy` -| 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`) | +- 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) -## Assets & Metadata +### `POST /properties/{propertyId}/bookings/{bookingId}/cancel` -| 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`) | +- 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) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 254e7c3..e48dada 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1,6 +1,6 @@ # API Reference -Generated from controller source. Use this for usage, params, response type, and behavior. +Generated from controller source. - Total endpoints: **125** - Auth: Firebase Bearer token unless endpoint is public. @@ -15,132 +15,130 @@ curl -X "https://api.hoteltrisolaris.in" \ -d '' ``` -Behavior notes in this file are handler summaries; strict business rules remain in controller code. - -| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Behavior | Handler | -|---|---|---|---|---|---|---|---|---| -| `GET` | `/` | `-` | `-` | `-` | `Map` | `200` | Root. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:14` (`root`) | -| `GET` | `/amenities` | `-` | `-` | `-` | `List` | `200` | List resources (list amenities). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:40` (`listAmenities`) | -| `POST` | `/amenities` | `-` | `-` | `AmenityUpsertRequest` | `AmenityResponse` | `201` | Create resource (create amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:49` (`createAmenity`) | -| `DELETE` | `/amenities/{amenityId}` | `amenityId:UUID` | `-` | `-` | `Unit` | `204` | Delete resource (delete amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:92` (`deleteAmenity`) | -| `PUT` | `/amenities/{amenityId}` | `amenityId:UUID` | `-` | `AmenityUpsertRequest` | `AmenityResponse` | `200` | Update resource (update amenity). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomAmenities.kt:68` (`updateAmenity`) | -| `GET` | `/auth/me` | `-` | `-` | `-` | `ResponseEntity` | `200` | Me. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:44` (`me`) | -| `PUT` | `/auth/me` | `-` | `-` | `UpdateMeRequest` | `ResponseEntity` | `200` | Update resource (update me). | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:54` (`updateMe`) | -| `POST` | `/auth/verify` | `-` | `-` | `-` | `ResponseEntity` | `200` | Verify. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:33` (`verify`) | -| `GET` | `/health` | `-` | `-` | `-` | `Map` | `200` | Health. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:9` (`health`) | -| `GET` | `/icons/png` | `-` | `-` | `-` | `List` | `200` | List resources (list png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:23` (`listPng`) | -| `GET` | `/icons/png/{filename}` | `filename:String` | `-` | `-` | `ResponseEntity` | `200` | Get resource (get png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:39` (`getPng`) | -| `GET` | `/image-tags` | `-` | `-` | `-` | `List` | `200` | List resources (list tags). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:35` (`listTags`) | -| `POST` | `/image-tags` | `-` | `-` | `RoomImageTagUpsertRequest` | `RoomImageTagResponse` | `201` | Create resource (create tag). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:43` (`createTag`) | -| `DELETE` | `/image-tags/{tagId}` | `tagId:UUID` | `-` | `-` | `Unit` | `204` | 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` | Update resource (update tag). | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomImageTags.kt:56` (`updateTag`) | -| `GET` | `/properties` | `-` | `-` | `-` | `List` | `200` | List resources (list properties). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:92` (`listProperties`) | -| `POST` | `/properties` | `-` | `-` | `PropertyCreateRequest` | `PropertyResponse` | `201` | Create resource (create property). | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:53` (`createProperty`) | -| `POST` | `/properties/access-codes/join` | `-` | `-` | `PropertyAccessCodeJoinRequest` | `PropertyUserResponse` | `200` | Join with access code. | `src/main/kotlin/com/android/trisolarisserver/controller/property/PropertyAccessCodes.kt:91` (`joinWithAccessCode`) | -| `PUT` | `/properties/{propertyId}` | `propertyId:UUID` | `-` | `PropertyUpdateRequest` | `PropertyResponse` | `200` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | Upsert. | `src/main/kotlin/com/android/trisolarisserver/controller/property/CancellationPolicies.kt:53` (`upsert`) | -| `GET` | `/properties/{propertyId}/code` | `propertyId:UUID` | `-` | `-` | `PropertyCodeResponse` | `200` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | Upsert settings. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpaySettingsController.kt:57` (`upsertSettings`) | -| `POST` | `/properties/{propertyId}/razorpay/return/failure` | `propertyId:UUID` | `-` | `-` | `Unit` | `204` | Failure. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:22` (`failure`) | -| `POST` | `/properties/{propertyId}/razorpay/return/success` | `propertyId:UUID` | `-` | `-` | `Unit` | `204` | Success. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayReturnController.kt:16` (`success`) | -| `POST` | `/properties/{propertyId}/razorpay/webhook` | `propertyId:UUID` | `-` | `String?` | `Unit` | `204` | Capture. | `src/main/kotlin/com/android/trisolarisserver/controller/razorpay/RazorpayWebhookCapture.kt:53` (`capture`) | -| `GET` | `/properties/{propertyId}/room-stays/active` | `propertyId:UUID` | `-` | `-` | `List` | `200` | 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` | 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` | 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` | 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` | 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` | 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` | Void room stay. | `src/main/kotlin/com/android/trisolarisserver/controller/room/RoomStays.kt:78` (`voidRoomStay`) | -| `GET` | `/properties/{propertyId}/room-types` | `propertyId:UUID` | `-` | `-` | `List` | `200` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | Room availability. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:126` (`roomAvailability`) | -| `GET` | `/properties/{propertyId}/rooms/availability-range` | `propertyId:UUID` | `-` | `-` | `List` | `200` | Room availability range. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:184` (`roomAvailabilityRange`) | -| `GET` | `/properties/{propertyId}/rooms/available` | `propertyId:UUID` | `-` | `-` | `List` | `200` | 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` | 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` | Room board. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:86` (`roomBoard`) | -| `GET` | `/properties/{propertyId}/rooms/board/stream` | `propertyId:UUID` | `-` | `-` | `SseEmitter` | `200` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | 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` | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:26` (`list`) | -| `GET` | `/properties/{propertyId}/users` | `propertyId:UUID` | `-` | `-` | `List` | `200` | 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` | 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` | 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` | 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` | Upsert property user roles. | `src/main/kotlin/com/android/trisolarisserver/controller/property/Properties.kt:189` (`upsertPropertyUserRoles`) | -| `GET` | `/users` | `-` | `phone:String? (optional)` | `-` | `List` | `200` | List resources (list app users). | `src/main/kotlin/com/android/trisolarisserver/controller/property/UserDirectory.kt:27` (`listAppUsers`) | +| 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 index 45601c2..efaac10 100644 --- a/scripts/generate_api_docs.py +++ b/scripts/generate_api_docs.py @@ -1,19 +1,20 @@ #!/usr/bin/env python3 -"""Generate API reference markdown from Spring controller annotations.""" +"""Generate API docs from Spring controller source.""" from __future__ import annotations import glob import os import re -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from typing import List +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" -OUTPUT = ROOT / "docs/API_REFERENCE.md" +REFERENCE_OUTPUT = ROOT / "docs/API_REFERENCE.md" +CATALOG_OUTPUT = ROOT / "docs/API_CATALOG.md" HTTP_BY_ANNOTATION = { "GetMapping": "GET", @@ -23,6 +24,116 @@ HTTP_BY_ANNOTATION = { "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: @@ -37,45 +148,16 @@ class Endpoint: handler_file: str handler_name: str handler_line: int - - -def method_from_annotations(annotations: List[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: List[str]) -> str: - for ann in annotations: - if "Mapping" in ann: - m = re.search(r'"([^"]*)"', ann) - if m: - return m.group(1) - return "" - - -def class_base_path(lines: List[str], class_line_index: int) -> str: - start = max(0, class_line_index - 50) - for i in range(start, class_line_index): - line = lines[i].strip() - if line.startswith("@RequestMapping"): - m = re.search(r'"([^"]+)"', line) - if m: - return m.group(1) - return "" + 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 = [] + buf: List[str] = [] angle = 0 paren = 0 bracket = 0 @@ -110,17 +192,53 @@ def split_params(param_blob: str) -> List[str]: return parts -def extract_types_from_params(param_blob: str) -> tuple[list[str], list[str], str]: +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 = " ".join(raw.split()) + 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: @@ -128,28 +246,31 @@ def extract_types_from_params(param_blob: str) -> tuple[list[str], list[str], st query_params.append(f"{param_name}:{param_type} ({required})") elif "@RequestBody" in segment: body_type = param_type - return path_params, query_params, body_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 explicit_status_from_annotations(annotations: List[str]) -> str | None: - for ann in annotations: - if ann.strip().startswith("@ResponseStatus"): - if "CREATED" in ann: - return "201" - if "NO_CONTENT" in ann: - return "204" - m = re.search(r"HttpStatus\.([A-Z_]+)", ann) - if m: - return m.group(1) - return None - - 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"): @@ -164,25 +285,239 @@ def behavior_from_name(name: str) -> str: 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 parse_endpoints(file_path: Path) -> List[Endpoint]: +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() - class_line = None - for idx, line in enumerate(lines): - if re.search(r"\bclass\b", line): - class_line = idx - break - if class_line is None: - return [] - base = class_base_path(lines, class_line) + 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() @@ -191,128 +526,239 @@ def parse_endpoints(file_path: Path) -> List[Endpoint]: i += 1 continue - if "fun " in stripped and pending_annotations: - method_names = method_from_annotations(pending_annotations) - if not method_names: - pending_annotations = [] - i += 1 - continue - - fun_line = i + 1 - signature = stripped - name_match = re.search(r"\bfun\s+(\w+)\s*\(", signature) - if not name_match: - pending_annotations = [] - i += 1 - continue - fun_name = name_match.group(1) - - # Parse function parameters with parenthesis depth, so annotation arguments - # like @RequestParam(required = false) do not break parsing. - line_idx = i - line_pos = lines[i].find("(") - depth = 1 - param_chars: List[str] = [] - tail_chars: List[str] = [] - while line_idx < len(lines): - current = lines[line_idx] - start = line_pos + 1 if line_idx == i else 0 - cursor = start - while cursor < len(current): - ch = current[cursor] - if depth > 0: - if ch == "(": - depth += 1 - param_chars.append(ch) - elif ch == ")": - depth -= 1 - if depth > 0: - param_chars.append(ch) - else: - param_chars.append(ch) - else: - tail_chars.append(ch) - cursor += 1 - if depth == 0: - # Capture any extra return type tokens from following lines until body opens. - if "{" not in "".join(tail_chars) and "=" not in "".join(tail_chars): - look = line_idx + 1 - while look < len(lines): - nxt = lines[look].strip() - if nxt.startswith("{") or nxt.startswith("="): - break - if nxt.startswith("@"): - break - tail_chars.append(" ") - tail_chars.append(nxt) - if "{" in nxt or "=" in nxt: - break - look += 1 + 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 - line_idx += 1 - - param_blob = "".join(param_chars).strip() - tail = "".join(tail_chars).strip() - path_params, query_params, body_type = extract_types_from_params(param_blob) - - return_type = "Unit" - rmatch = re.search(r":\s*([^{=]+)", tail) - if rmatch: - return_type = rmatch.group(1).strip() - - rel_path = mapping_path(pending_annotations) - full_path = "/" + "/".join([p.strip("/") for p in [base, rel_path] if p]) - if full_path == "": - full_path = "/" - - explicit_status = explicit_status_from_annotations(pending_annotations) - behavior = behavior_from_name(fun_name) - rel_file = os.path.relpath(file_path, ROOT) - - for method in method_names: - endpoints.append( - Endpoint( - method=method, - path=full_path or "/", - 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=fun_name, - handler_line=fun_line, - ) - ) + if mapped_base: + current_base_path = mapped_base pending_annotations = [] - i = line_idx + 1 + 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 + return endpoints, functions -def main() -> None: - endpoints: List[Endpoint] = [] - for file_name in sorted(glob.glob(str(CONTROLLER_ROOT / "**/*.kt"), recursive=True)): - endpoints.extend(parse_endpoints(Path(file_name))) +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) - uniq = {} - for e in endpoints: - key = (e.method, e.path, e.handler_file, e.handler_name) - uniq[key] = e - ordered = sorted(uniq.values(), key=lambda e: (e.path, e.method, e.handler_file, e.handler_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. Use this for usage, params, response type, and behavior.", + "Generated from controller source.", "", - f"- Total endpoints: **{len(ordered)}**", + f"- Total endpoints: **{len(endpoints)}**", "- Auth: Firebase Bearer token unless endpoint is public.", "- Regenerate: `python scripts/generate_api_docs.py`", "", @@ -325,23 +771,117 @@ def main() -> None: " -d ''", "```", "", - "Behavior notes in this file are handler summaries; strict business rules remain in controller code.", - "", - "| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Behavior | Handler |", - "|---|---|---|---|---|---|---|---|---|", + "| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Auth | Common Errors | Behavior | Handler |", + "|---|---|---|---|---|---|---|---|---|---|---|", ] - for e in ordered: - path_params = ", ".join(e.path_params) if e.path_params else "-" - query_params = ", ".join(e.query_params) if e.query_params else "-" - handler = f"`{e.handler_file}:{e.handler_line}` (`{e.handler_name}`)" + 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"| `{e.method}` | `{e.path}` | `{path_params}` | `{query_params}` | `{e.body_type}` | `{e.response_type}` | `{e.status}` | {e.behavior} | {handler} |" + f"| `{endpoint.method}` | `{endpoint.path}` | `{path_params}` | `{query_params}` | `{endpoint.body_type}` | `{endpoint.response_type}` | `{endpoint.status}` | {endpoint.auth} | {errors_text} | {endpoint.behavior} | {handler} |" ) - OUTPUT.parent.mkdir(parents=True, exist_ok=True) - OUTPUT.write_text("\n".join(lines) + "\n", encoding="utf-8") - print(f"Wrote {OUTPUT} ({len(ordered)} endpoints)") + 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__":