Generate behavior-first API catalog from controllers
All checks were successful
build-and-deploy / build-deploy (push) Successful in 17s

This commit is contained in:
androidlover5842
2026-02-04 12:21:48 +05:30
parent fdb6792018
commit 35680287d4
3 changed files with 2398 additions and 481 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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 <METHOD> "https://api.hoteltrisolaris.in<PATH>" \
-d '<REQUEST_BODY_JSON>'
```
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<String, String>` | `200` | Root. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:14` (`root`) |
| `GET` | `/amenities` | `-` | `-` | `-` | `List<AmenityResponse>` | `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<AuthResponse>` | `200` | Me. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:44` (`me`) |
| `PUT` | `/auth/me` | `-` | `-` | `UpdateMeRequest` | `ResponseEntity<AuthResponse>` | `200` | Update resource (update me). | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:54` (`updateMe`) |
| `POST` | `/auth/verify` | `-` | `-` | `-` | `ResponseEntity<AuthResponse>` | `200` | Verify. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:33` (`verify`) |
| `GET` | `/health` | `-` | `-` | `-` | `Map<String, String>` | `200` | Health. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:9` (`health`) |
| `GET` | `/icons/png` | `-` | `-` | `-` | `List<String>` | `200` | List resources (list png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:23` (`listPng`) |
| `GET` | `/icons/png/{filename}` | `filename:String` | `-` | `-` | `ResponseEntity<FileSystemResource>` | `200` | Get resource (get png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:39` (`getPng`) |
| `GET` | `/image-tags` | `-` | `-` | `-` | `List<RoomImageTagResponse>` | `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<PropertyResponse>` | `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<BookingListItem>` | `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<ChargeResponse>` | `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<PaymentResponse>` | `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<RazorpayQrRecordResponse>` | `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<RazorpayQrEventResponse>` | `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<RazorpayPaymentRequestResponse>` | `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<BookingRoomRequestResponse>` | `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<GuestResponse>` | `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<GuestDocumentResponse>` | `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<FileSystemResource>` | `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<GuestRatingResponse>` | `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<FileSystemResource>` | `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<FileSystemResource>` | `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<RatePlanResponse>` | `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<RateCalendarResponse>` | `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<ActiveRoomStayResponse>` | `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<IssuedCardResponse>` | `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<RoomTypeResponse>` | `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<RoomImageResponse>` | `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<RoomResponse>` | `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<RoomAvailabilityResponse>` | `200` | Room availability. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:126` (`roomAvailability`) |
| `GET` | `/properties/{propertyId}/rooms/availability-range` | `propertyId:UUID` | `-` | `-` | `List<RoomAvailabilityRangeResponse>` | `200` | Room availability range. | `src/main/kotlin/com/android/trisolarisserver/controller/room/Rooms.kt:184` (`roomAvailabilityRange`) |
| `GET` | `/properties/{propertyId}/rooms/available` | `propertyId:UUID` | `-` | `-` | `List<RoomResponse>` | `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<RoomAvailabilityWithRateResponse>` | `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<RoomBoardResponse>` | `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<RoomResponse>` | `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<RoomImageResponse>` | `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<UUID>? (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<FileSystemResource>` | `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<TransportModeStatusResponse>` | `200` | List resources (list). | `src/main/kotlin/com/android/trisolarisserver/controller/transport/TransportModes.kt:26` (`list`) |
| `GET` | `/properties/{propertyId}/users` | `propertyId:UUID` | `-` | `-` | `List<PropertyUserResponse>` | `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<PropertyUserDetailsResponse>` | `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<AppUserSummaryResponse>` | `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<String, String>` | `200` | Public/unspecified | - | Root. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:14` (`root`) |
| `GET` | `/amenities` | `-` | `-` | `-` | `List<AmenityResponse>` | `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<AuthResponse>` | `200` | Authenticated user (Firebase) | - | Me. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:44` (`me`) |
| `PUT` | `/auth/me` | `-` | `-` | `UpdateMeRequest` | `ResponseEntity<AuthResponse>` | `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<AuthResponse>` | `200` | Authenticated user (Firebase) | - | Verify. | `src/main/kotlin/com/android/trisolarisserver/controller/auth/Auth.kt:33` (`verify`) |
| `GET` | `/health` | `-` | `-` | `-` | `Map<String, String>` | `200` | Public/unspecified | - | Health. | `src/main/kotlin/com/android/trisolarisserver/controller/system/Health.kt:9` (`health`) |
| `GET` | `/icons/png` | `-` | `-` | `-` | `List<String>` | `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<FileSystemResource>` | `200` | Public/unspecified | - | Get resource (get png). | `src/main/kotlin/com/android/trisolarisserver/controller/assets/IconFiles.kt:39` (`getPng`) |
| `GET` | `/image-tags` | `-` | `-` | `-` | `List<RoomImageTagResponse>` | `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<PropertyResponse>` | `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<BookingListItem>` | `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<ChargeResponse>` | `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<PaymentResponse>` | `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<RazorpayQrRecordResponse>` | `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<RazorpayQrEventResponse>` | `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<RazorpayPaymentRequestResponse>` | `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<BookingRoomRequestResponse>` | `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<GuestResponse>` | `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<GuestDocumentResponse>` | `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<FileSystemResource>` | `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<GuestRatingResponse>` | `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<FileSystemResource>` | `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<FileSystemResource>` | `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<RatePlanResponse>` | `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<RateCalendarResponse>` | `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<ActiveRoomStayResponse>` | `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<IssuedCardResponse>` | `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<RoomTypeResponse>` | `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<RoomImageResponse>` | `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<RoomResponse>` | `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<RoomAvailabilityResponse>` | `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<RoomAvailabilityRangeResponse>` | `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<RoomResponse>` | `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<RoomAvailabilityWithRateResponse>` | `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<RoomBoardResponse>` | `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<RoomResponse>` | `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<RoomImageResponse>` | `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<UUID>? (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<FileSystemResource>` | `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<TransportModeStatusResponse>` | `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<PropertyUserResponse>` | `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<PropertyUserDetailsResponse>` | `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<AppUserSummaryResponse>` | `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`) |

View File

@@ -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 default_status(method: str, response_type: str, explicit: str | None) -> str:
if explicit:
return explicit
return "200"
def explicit_status_from_annotations(annotations: List[str]) -> str | None:
def explicit_status_from_annotations(annotations: Sequence[str]) -> str | None:
for ann in annotations:
if ann.strip().startswith("@ResponseStatus"):
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 m.group(1)
return http_code(m.group(1))
return None
def default_status(method: str, response_type: str, explicit: str | None) -> str:
if explicit:
return explicit
if method == "DELETE" and response_type == "Unit":
return "204"
return "200"
def behavior_from_name(name: str) -> str:
words = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name).lower()
if name.startswith("list"):
@@ -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,89 +526,57 @@ 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:
class_match = re.search(r"\bclass\b", stripped)
if class_match:
mapped_base = ""
for ann in pending_annotations:
if ann.startswith("@RequestMapping"):
mapped_base = mapping_path([ann])
break
if mapped_base:
current_base_path = mapped_base
pending_annotations = []
i += 1
continue
fun_line = i + 1
signature = stripped
name_match = re.search(r"\bfun\s+(\w+)\s*\(", signature)
if not name_match:
fun_match = re.search(r"\bfun\s+(\w+)\s*\(", stripped)
if fun_match:
name = fun_match.group(1)
function_annotations = pending_annotations[:]
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
break
line_idx += 1
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)
param_blob = "".join(param_chars).strip()
tail = "".join(tail_chars).strip()
path_params, query_params, body_type = extract_types_from_params(param_blob)
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,
)
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)
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 or "/",
path=full_path,
path_params=path_params,
query_params=query_params,
body_type=body_type,
@@ -281,38 +584,181 @@ def parse_endpoints(file_path: Path) -> List[Endpoint]:
status=default_status(method, return_type, explicit_status),
behavior=behavior,
handler_file=rel_file,
handler_name=fun_name,
handler_line=fun_line,
handler_name=name,
handler_line=i + 1,
)
)
pending_annotations = []
i = line_idx + 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 '<REQUEST_BODY_JSON>'",
"```",
"",
"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__":