Remove generated API reference tooling and keep manual catalog
All checks were successful
build-and-deploy / build-deploy (push) Successful in 17s

This commit is contained in:
androidlover5842
2026-02-04 12:30:45 +05:30
parent 35680287d4
commit 730a1d782f
3 changed files with 193 additions and 2603 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +0,0 @@
# API Reference
Generated from controller source.
- Total endpoints: **125**
- Auth: Firebase Bearer token unless endpoint is public.
- Regenerate: `python scripts/generate_api_docs.py`
## Usage Template
```bash
curl -X <METHOD> "https://api.hoteltrisolaris.in<PATH>" \
-H "Authorization: Bearer <FIREBASE_ID_TOKEN>" \
-H "Content-Type: application/json" \
-d '<REQUEST_BODY_JSON>'
```
| 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,888 +0,0 @@
#!/usr/bin/env python3
"""Generate API docs from Spring controller source."""
from __future__ import annotations
import glob
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Iterable, List, Sequence, Set, Tuple
ROOT = Path(__file__).resolve().parents[1]
CONTROLLER_ROOT = ROOT / "src/main/kotlin/com/android/trisolarisserver/controller"
REFERENCE_OUTPUT = ROOT / "docs/API_REFERENCE.md"
CATALOG_OUTPUT = ROOT / "docs/API_CATALOG.md"
HTTP_BY_ANNOTATION = {
"GetMapping": "GET",
"PostMapping": "POST",
"PutMapping": "PUT",
"DeleteMapping": "DELETE",
"PatchMapping": "PATCH",
}
HTTP_STATUS_CODE = {
"CONTINUE": "100",
"SWITCHING_PROTOCOLS": "101",
"OK": "200",
"CREATED": "201",
"ACCEPTED": "202",
"NO_CONTENT": "204",
"MOVED_PERMANENTLY": "301",
"FOUND": "302",
"BAD_REQUEST": "400",
"UNAUTHORIZED": "401",
"FORBIDDEN": "403",
"NOT_FOUND": "404",
"METHOD_NOT_ALLOWED": "405",
"CONFLICT": "409",
"UNPROCESSABLE_ENTITY": "422",
"TOO_MANY_REQUESTS": "429",
"INTERNAL_SERVER_ERROR": "500",
"BAD_GATEWAY": "502",
"SERVICE_UNAVAILABLE": "503",
}
KNOWN_HELPER_ERRORS: Dict[str, List[Tuple[str, str]]] = {
"requirePrincipal": [("UNAUTHORIZED", "Missing principal")],
"requireUser": [("UNAUTHORIZED", "User not found")],
"requireMember": [
("UNAUTHORIZED", "Missing principal"),
("FORBIDDEN", "Property membership required"),
],
"requireRole": [
("UNAUTHORIZED", "Missing principal"),
("FORBIDDEN", "Required property role not granted"),
],
"requireSuperAdmin": [
("UNAUTHORIZED", "User not found"),
("FORBIDDEN", "Super admin only"),
],
"requireProperty": [("NOT_FOUND", "Property not found")],
"requirePropertyGuest": [
("NOT_FOUND", "Property or guest not found"),
("BAD_REQUEST", "Guest not in property"),
],
"requireRoomStayForProperty": [("NOT_FOUND", "Room stay not found for property")],
"requireOpenRoomStayForProperty": [
("NOT_FOUND", "Room stay not found for property"),
("CONFLICT", "Room stay is already closed"),
],
"parseOffset": [("BAD_REQUEST", "Invalid timestamp")],
"parseDate": [("BAD_REQUEST", "Invalid date format")],
}
KNOWN_SIDE_EFFECTS = (
("bookingEvents.emit", "Emits booking SSE updates."),
("roomBoardEvents.emit", "Emits room board SSE updates."),
("roomBoardEvents.emitRoom", "Emits room board SSE updates."),
("subscribe(", "Streams SSE events."),
("logStayAudit(", "Writes room-stay audit log."),
("roomStayAuditLogRepo.save", "Writes room-stay audit log."),
("guestDocumentRepo.save", "Stores/updates guest document metadata."),
("guestDocumentRepo.delete", "Deletes guest document metadata."),
("storageService.store", "Stores file payload on configured storage."),
("storageService.delete", "Deletes file payload from configured storage."),
)
KEYWORDS = {
"if",
"for",
"while",
"when",
"return",
"throw",
"catch",
"try",
"else",
"do",
"super",
"this",
"listOf",
"mapOf",
"setOf",
"mutableListOf",
"mutableMapOf",
"mutableSetOf",
"arrayOf",
"require",
"check",
"println",
}
@dataclass
class DtoField:
name: str
type_name: str
optional: bool
@dataclass
class FunctionInfo:
name: str
line: int
annotations: List[str]
param_blob: str
response_type: str
body: str
calls: Set[str]
errors: List[Tuple[str, str]]
roles: Set[str]
has_principal: bool
@dataclass
class Endpoint:
method: str
path: str
path_params: List[str]
query_params: List[str]
body_type: str
response_type: str
status: str
behavior: str
handler_file: str
handler_name: str
handler_line: int
auth: str = "-"
validation_notes: List[str] = field(default_factory=list)
common_errors: List[Tuple[str, List[str]]] = field(default_factory=list)
side_effects: List[str] = field(default_factory=list)
body_shape: str = "-"
def split_params(param_blob: str) -> List[str]:
parts: List[str] = []
buf: List[str] = []
angle = 0
paren = 0
bracket = 0
brace = 0
for ch in param_blob:
if ch == "<":
angle += 1
elif ch == ">":
angle = max(0, angle - 1)
elif ch == "(":
paren += 1
elif ch == ")":
paren = max(0, paren - 1)
elif ch == "[":
bracket += 1
elif ch == "]":
bracket = max(0, bracket - 1)
elif ch == "{":
brace += 1
elif ch == "}":
brace = max(0, brace - 1)
if ch == "," and angle == 0 and paren == 0 and bracket == 0 and brace == 0:
part = "".join(buf).strip()
if part:
parts.append(part)
buf = []
continue
buf.append(ch)
tail = "".join(buf).strip()
if tail:
parts.append(tail)
return parts
def normalize_space(value: str) -> str:
return " ".join(value.split())
def http_code(value: str) -> str:
if value.isdigit():
return value
return HTTP_STATUS_CODE.get(value, value)
def method_from_annotations(annotations: Sequence[str]) -> List[str]:
for ann in annotations:
m = re.match(r"@(\w+)", ann.strip())
if not m:
continue
name = m.group(1)
if name in HTTP_BY_ANNOTATION:
return [HTTP_BY_ANNOTATION[name]]
if name == "RequestMapping":
methods = re.findall(r"RequestMethod\.(GET|POST|PUT|DELETE|PATCH)", ann)
return methods or ["ANY"]
return []
def mapping_path(annotations: Sequence[str]) -> str:
for ann in annotations:
if "Mapping" not in ann:
continue
m = re.search(r'"([^"]*)"', ann)
if m:
return m.group(1)
return ""
def extract_types_from_params(param_blob: str) -> Tuple[List[str], List[str], str, bool]:
path_params: List[str] = []
query_params: List[str] = []
body_type = "-"
has_principal = False
for raw in split_params(param_blob):
segment = normalize_space(raw)
name_match = re.search(r"(\w+)\s*:", segment)
param_name = name_match.group(1) if name_match else "param"
type_match = re.search(r":\s*([^=]+)", segment)
param_type = type_match.group(1).strip() if type_match else "Unknown"
if "@AuthenticationPrincipal" in segment:
has_principal = True
if "@PathVariable" in segment:
path_params.append(f"{param_name}:{param_type}")
elif "@RequestParam" in segment:
required = "optional" if "required = false" in segment else "required"
query_params.append(f"{param_name}:{param_type} ({required})")
elif "@RequestBody" in segment:
body_type = param_type
return path_params, query_params, body_type, has_principal
def explicit_status_from_annotations(annotations: Sequence[str]) -> str | None:
for ann in annotations:
if not ann.strip().startswith("@ResponseStatus"):
continue
if "CREATED" in ann:
return "201"
if "NO_CONTENT" in ann:
return "204"
m = re.search(r"HttpStatus\.([A-Z_]+)", ann)
if m:
return http_code(m.group(1))
return None
def default_status(method: str, response_type: str, explicit: str | None) -> str:
if explicit:
return explicit
if method == "DELETE" and response_type == "Unit":
return "204"
return "200"
def behavior_from_name(name: str) -> str:
words = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name).lower()
if name.startswith("list"):
return f"List resources ({words})."
if name.startswith("create"):
return f"Create resource ({words})."
if name.startswith("update"):
return f"Update resource ({words})."
if name.startswith("delete"):
return f"Delete resource ({words})."
if name.startswith("get"):
return f"Get resource ({words})."
if name.startswith("stream"):
return f"Stream events/data ({words})."
if name.startswith("checkOut"):
return f"Check out flow ({words})."
if name.startswith("checkIn"):
return f"Check in flow ({words})."
if name.startswith("cancel"):
return f"Cancel flow ({words})."
if name.startswith("noShow"):
return f"No-show flow ({words})."
return f"{words.capitalize()}."
def join_paths(base: str, rel: str) -> str:
if not base and not rel:
return "/"
parts = [p.strip("/") for p in (base, rel) if p]
return "/" + "/".join(parts)
def parse_return_type(tail: str) -> str:
m = re.search(r":\s*([^{=]+)", tail)
if not m:
return "Unit"
return m.group(1).strip()
def find_matching_paren(text: str, open_index: int) -> int:
depth = 0
for idx in range(open_index, len(text)):
ch = text[idx]
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
return idx
return -1
def extract_parenthesized(text: str, open_index: int) -> str:
close_index = find_matching_paren(text, open_index)
if close_index == -1:
return ""
return text[open_index + 1 : close_index]
def strip_leading_annotations(segment: str) -> str:
value = segment.strip()
while value.startswith("@"):
if "(" in value and (value.find("(") < value.find(" ") if " " in value else True):
open_index = value.find("(")
close_index = find_matching_paren(value, open_index)
if close_index == -1:
break
value = value[close_index + 1 :].strip()
else:
value = re.sub(r"^@\w+\s*", "", value).strip()
return value
def extract_calls(body: str) -> Set[str]:
calls = set()
for name in re.findall(r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\(", body):
if name in KEYWORDS:
continue
calls.add(name)
return calls
def extract_errors(body: str) -> List[Tuple[str, str]]:
errors: List[Tuple[str, str]] = []
pattern = re.compile(
r"ResponseStatusException\(\s*HttpStatus\.([A-Z_]+)\s*,\s*\"([^\"]+)\"",
re.DOTALL,
)
for status, message in pattern.findall(body):
errors.append((status, normalize_space(message)))
return errors
def extract_roles(body: str) -> Set[str]:
return set(re.findall(r"Role\.([A-Z_]+)", body))
def parse_function_signature(lines: List[str], start_line: int) -> Tuple[str, str, int, int, str]:
current_line = lines[start_line]
open_col = current_line.find("(")
if open_col == -1:
return "", "Unit", start_line, len(current_line), ""
depth = 1
param_chars: List[str] = []
line_idx = start_line
close_col = open_col
while line_idx < len(lines):
line = lines[line_idx]
start = open_col + 1 if line_idx == start_line else 0
col = start
while col < len(line):
ch = line[col]
if depth > 0:
if ch == "(":
depth += 1
param_chars.append(ch)
elif ch == ")":
depth -= 1
if depth == 0:
close_col = col
break
param_chars.append(ch)
else:
param_chars.append(ch)
col += 1
if depth == 0:
break
line_idx += 1
tail_parts: List[str] = []
if line_idx < len(lines):
tail_parts.append(lines[line_idx][close_col + 1 :])
look = line_idx + 1
while look < len(lines):
trimmed = lines[look].strip()
if not trimmed:
tail_parts.append(" ")
look += 1
continue
if trimmed.startswith("@"):
break
if trimmed.startswith("fun ") or trimmed.startswith("private fun ") or trimmed.startswith("internal fun "):
break
tail_parts.append(" " + trimmed)
if "{" in trimmed or "=" in trimmed:
break
look += 1
tail = "".join(tail_parts).strip()
return "".join(param_chars).strip(), parse_return_type(tail), line_idx, close_col, tail
def parse_function_body(lines: List[str], signature_end_line: int, signature_end_col: int) -> Tuple[str, int]:
line_idx = signature_end_line
col = signature_end_col + 1
marker_line = -1
marker_col = -1
marker = ""
while line_idx < len(lines):
line = lines[line_idx]
start = col if line_idx == signature_end_line else 0
for idx in range(start, len(line)):
ch = line[idx]
if ch == "{":
marker_line = line_idx
marker_col = idx
marker = "{"
break
if ch == "=":
marker_line = line_idx
marker_col = idx
marker = "="
break
if marker:
break
line_idx += 1
if not marker:
return "", signature_end_line
if marker == "=":
expression = lines[marker_line][marker_col + 1 :].strip()
return expression, marker_line
body_parts: List[str] = []
depth = 1
for li in range(marker_line, len(lines)):
line = lines[li]
start = marker_col + 1 if li == marker_line else 0
segment_start = start
for cj in range(start, len(line)):
ch = line[cj]
if ch == "{":
if depth >= 1 and segment_start <= cj:
body_parts.append(line[segment_start:cj])
depth += 1
segment_start = cj + 1
elif ch == "}":
if depth >= 1 and segment_start <= cj:
body_parts.append(line[segment_start:cj])
depth -= 1
if depth == 0:
return "".join(body_parts), li
segment_start = cj + 1
if depth >= 1:
body_parts.append(line[segment_start:])
body_parts.append("\n")
return "".join(body_parts), len(lines) - 1
def parse_data_classes() -> Dict[str, List[DtoField]]:
dto_map: Dict[str, List[DtoField]] = {}
for file_name in sorted(glob.glob(str(CONTROLLER_ROOT / "**/*.kt"), recursive=True)):
text = Path(file_name).read_text(encoding="utf-8")
for match in re.finditer(r"\bdata class\s+(\w+)\s*\(", text):
class_name = match.group(1)
open_index = text.find("(", match.start())
if open_index == -1:
continue
blob = extract_parenthesized(text, open_index)
fields: List[DtoField] = []
for raw in split_params(blob):
cleaned = strip_leading_annotations(raw)
if not cleaned:
continue
field_match = re.search(r"(?:val|var)\s+(\w+)\s*:\s*([^=]+?)(?:\s*=\s*.+)?$", cleaned)
if not field_match:
continue
field_name = field_match.group(1)
type_name = field_match.group(2).strip()
optional = "?" in type_name or "=" in cleaned
fields.append(DtoField(field_name, type_name, optional))
if fields:
dto_map[class_name] = fields
return dto_map
def parse_controller_file(file_path: Path) -> Tuple[List[Endpoint], Dict[str, FunctionInfo]]:
text = file_path.read_text(encoding="utf-8")
if "@RestController" not in text and "@Controller" not in text:
return [], {}
lines = text.splitlines()
endpoints: List[Endpoint] = []
functions: Dict[str, FunctionInfo] = {}
pending_annotations: List[str] = []
current_base_path = ""
i = 0
while i < len(lines):
stripped = lines[i].strip()
if stripped.startswith("@"):
pending_annotations.append(stripped)
i += 1
continue
class_match = re.search(r"\bclass\b", stripped)
if class_match:
mapped_base = ""
for ann in pending_annotations:
if ann.startswith("@RequestMapping"):
mapped_base = mapping_path([ann])
break
if mapped_base:
current_base_path = mapped_base
pending_annotations = []
i += 1
continue
fun_match = re.search(r"\bfun\s+(\w+)\s*\(", stripped)
if fun_match:
name = fun_match.group(1)
function_annotations = pending_annotations[:]
pending_annotations = []
param_blob, return_type, sig_end_line, sig_end_col, _ = parse_function_signature(lines, i)
body, body_end_line = parse_function_body(lines, sig_end_line, sig_end_col)
path_params, query_params, body_type, has_principal = extract_types_from_params(param_blob)
direct_errors = extract_errors(body)
roles = extract_roles(body)
calls = extract_calls(body)
functions[name] = FunctionInfo(
name=name,
line=i + 1,
annotations=function_annotations,
param_blob=param_blob,
response_type=return_type,
body=body,
calls=calls,
errors=direct_errors,
roles=roles,
has_principal=has_principal,
)
method_names = method_from_annotations(function_annotations)
if method_names:
rel_path = mapping_path(function_annotations)
full_path = join_paths(current_base_path, rel_path)
explicit_status = explicit_status_from_annotations(function_annotations)
behavior = behavior_from_name(name)
rel_file = os.path.relpath(file_path, ROOT)
for method in method_names:
endpoints.append(
Endpoint(
method=method,
path=full_path,
path_params=path_params,
query_params=query_params,
body_type=body_type,
response_type=return_type,
status=default_status(method, return_type, explicit_status),
behavior=behavior,
handler_file=rel_file,
handler_name=name,
handler_line=i + 1,
)
)
i = body_end_line + 1
continue
if stripped and not stripped.startswith("//"):
pending_annotations = []
i += 1
return endpoints, functions
def collect_function_metadata(
function_name: str,
functions: Dict[str, FunctionInfo],
visiting: Set[str] | None = None,
) -> Tuple[List[Tuple[str, str]], Set[str], Set[str], Set[str]]:
if visiting is None:
visiting = set()
if function_name in visiting:
return [], set(), set(), set()
func = functions.get(function_name)
if not func:
return [], set(), set(), set()
visiting.add(function_name)
errors = list(func.errors)
roles = set(func.roles)
calls = set(func.calls)
auth_hints: Set[str] = set()
if "requireSuperAdmin" in calls:
auth_hints.add("SUPER_ADMIN")
if "requireMember" in calls:
auth_hints.add("PROPERTY_MEMBER")
if "requireRole" in calls and roles:
auth_hints.add("ROLE_BASED")
if func.has_principal:
auth_hints.add("AUTHENTICATED")
for call in list(calls):
if call in KNOWN_HELPER_ERRORS:
errors.extend(KNOWN_HELPER_ERRORS[call])
if call in functions and call != function_name:
child_errors, child_roles, child_calls, child_auth_hints = collect_function_metadata(
call, functions, visiting
)
errors.extend(child_errors)
roles.update(child_roles)
calls.update(child_calls)
auth_hints.update(child_auth_hints)
return errors, roles, calls, auth_hints
def unique_messages(values: Iterable[str]) -> List[str]:
seen = set()
out: List[str] = []
for value in values:
key = value.strip()
if not key or key in seen:
continue
seen.add(key)
out.append(key)
return out
def resolve_auth_label(
roles: Set[str],
calls: Set[str],
auth_hints: Set[str],
has_principal: bool,
) -> str:
if "SUPER_ADMIN" in auth_hints:
return "SUPER_ADMIN"
if roles:
return "Roles: " + ", ".join(sorted(roles))
if "PROPERTY_MEMBER" in auth_hints:
return "Any property member"
if has_principal or "AUTHENTICATED" in auth_hints:
return "Authenticated user (Firebase)"
return "Public/unspecified"
def summarize_errors(errors: Sequence[Tuple[str, str]]) -> List[Tuple[str, List[str]]]:
grouped: Dict[str, List[str]] = {}
for status, message in errors:
code = http_code(status)
grouped.setdefault(code, []).append(message)
ordering = ["401", "403", "404", "400", "409", "422", "429", "500"]
ordered_codes = sorted(grouped.keys(), key=lambda code: (ordering.index(code) if code in ordering else 999, code))
summary: List[Tuple[str, List[str]]] = []
for code in ordered_codes:
messages = unique_messages(grouped[code])[:4]
summary.append((code, messages))
return summary
def resolve_body_shape(body_type: str, dto_fields: Dict[str, List[DtoField]]) -> str:
if body_type == "-":
return "-"
clean = body_type.strip().replace("?", "")
candidates = [clean]
if "." in clean:
candidates.append(clean.split(".")[-1])
if "<" in clean and ">" in clean:
inner = clean[clean.find("<") + 1 : clean.rfind(">")].strip()
candidates.append(inner)
if "." in inner:
candidates.append(inner.split(".")[-1])
for candidate in candidates:
fields = dto_fields.get(candidate)
if not fields:
continue
chunks = []
for field in fields:
label = f"{field.name}:{field.type_name}"
if field.optional:
label += " (optional)"
chunks.append(label)
joined = ", ".join(chunks)
return f"{candidate} {{ {joined} }}"
return body_type
def resolve_side_effects(body: str, response_type: str) -> List[str]:
effects = []
if "SseEmitter" in response_type:
effects.append("Streams SSE events.")
for pattern, description in KNOWN_SIDE_EFFECTS:
if pattern in body:
effects.append(description)
return unique_messages(effects)
def enrich_endpoints(
endpoints: List[Endpoint],
file_function_map: Dict[str, Dict[str, FunctionInfo]],
dto_fields: Dict[str, List[DtoField]],
) -> List[Endpoint]:
enriched: List[Endpoint] = []
for endpoint in endpoints:
functions = file_function_map.get(endpoint.handler_file, {})
function_info = functions.get(endpoint.handler_name)
if function_info is None:
enriched.append(endpoint)
continue
errors, roles, calls, auth_hints = collect_function_metadata(endpoint.handler_name, functions)
error_summary = summarize_errors(errors)
validations = []
for code, messages in error_summary:
if code not in {"400", "409", "422"}:
continue
for message in messages:
validations.append(f"{code}: {message}")
endpoint.auth = resolve_auth_label(roles, calls, auth_hints, function_info.has_principal)
endpoint.common_errors = error_summary
endpoint.validation_notes = validations
endpoint.body_shape = resolve_body_shape(endpoint.body_type, dto_fields)
endpoint.side_effects = resolve_side_effects(function_info.body, endpoint.response_type)
enriched.append(endpoint)
return enriched
def write_reference(endpoints: Sequence[Endpoint]) -> None:
lines = [
"# API Reference",
"",
"Generated from controller source.",
"",
f"- Total endpoints: **{len(endpoints)}**",
"- Auth: Firebase Bearer token unless endpoint is public.",
"- Regenerate: `python scripts/generate_api_docs.py`",
"",
"## Usage Template",
"",
"```bash",
"curl -X <METHOD> \"https://api.hoteltrisolaris.in<PATH>\" \\",
" -H \"Authorization: Bearer <FIREBASE_ID_TOKEN>\" \\",
" -H \"Content-Type: application/json\" \\",
" -d '<REQUEST_BODY_JSON>'",
"```",
"",
"| Method | Path | Path Params | Query Params | Body Type | Response Type | Status | Auth | Common Errors | Behavior | Handler |",
"|---|---|---|---|---|---|---|---|---|---|---|",
]
for endpoint in endpoints:
path_params = ", ".join(endpoint.path_params) if endpoint.path_params else "-"
query_params = ", ".join(endpoint.query_params) if endpoint.query_params else "-"
handler = f"`{endpoint.handler_file}:{endpoint.handler_line}` (`{endpoint.handler_name}`)"
if endpoint.common_errors:
parts = []
for code, messages in endpoint.common_errors:
if messages:
parts.append(f"{code} ({'; '.join(messages[:2])})")
else:
parts.append(code)
errors_text = ", ".join(parts)
else:
errors_text = "-"
lines.append(
f"| `{endpoint.method}` | `{endpoint.path}` | `{path_params}` | `{query_params}` | `{endpoint.body_type}` | `{endpoint.response_type}` | `{endpoint.status}` | {endpoint.auth} | {errors_text} | {endpoint.behavior} | {handler} |"
)
REFERENCE_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
REFERENCE_OUTPUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_catalog(endpoints: Sequence[Endpoint]) -> None:
grouped: Dict[str, List[Endpoint]] = {}
for endpoint in endpoints:
grouped.setdefault(endpoint.handler_file, []).append(endpoint)
lines = [
"# API Catalog",
"",
"Behavior-first catalog generated from controller source.",
"",
f"- Total endpoints: **{len(endpoints)}**",
"- Notes: validations/errors are extracted from explicit `ResponseStatusException` checks and shared helper guards.",
"- Regenerate: `python scripts/generate_api_docs.py`",
"",
]
for handler_file in sorted(grouped.keys()):
entries = sorted(grouped[handler_file], key=lambda e: (e.path, e.method, e.handler_name))
lines.append(f"## `{handler_file}`")
lines.append("")
for endpoint in entries:
lines.append(f"### `{endpoint.method} {endpoint.path}`")
lines.append("")
lines.append(f"- Handler: `{endpoint.handler_name}` (`{endpoint.handler_file}:{endpoint.handler_line}`)")
lines.append(f"- Behavior: {endpoint.behavior}")
if endpoint.path_params:
lines.append(f"- Path params: {', '.join(endpoint.path_params)}")
if endpoint.query_params:
lines.append(f"- Query params: {', '.join(endpoint.query_params)}")
if endpoint.body_type == "-":
lines.append("- Body: none")
else:
lines.append(f"- Body: {endpoint.body_shape}")
if endpoint.side_effects:
lines.append(f"- Side effects: {' '.join(endpoint.side_effects)}")
lines.append(f"- Auth: {endpoint.auth}")
lines.append(f"- Response: `{endpoint.status}` `{endpoint.response_type}`")
if endpoint.validation_notes:
lines.append("- Validation/guard checks:")
for note in endpoint.validation_notes:
lines.append(f" - {note}")
if endpoint.common_errors:
summary = []
for code, messages in endpoint.common_errors:
if messages:
summary.append(f"{code} ({'; '.join(messages[:2])})")
else:
summary.append(code)
lines.append(f"- Common errors: {', '.join(summary)}")
else:
lines.append("- Common errors: none observed in controller checks")
lines.append("")
CATALOG_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
CATALOG_OUTPUT.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
def main() -> None:
dto_fields = parse_data_classes()
all_endpoints: List[Endpoint] = []
file_function_map: Dict[str, Dict[str, FunctionInfo]] = {}
for file_name in sorted(glob.glob(str(CONTROLLER_ROOT / "**/*.kt"), recursive=True)):
file_path = Path(file_name)
endpoints, functions = parse_controller_file(file_path)
rel_file = os.path.relpath(file_path, ROOT)
if endpoints:
all_endpoints.extend(endpoints)
if functions:
file_function_map[rel_file] = functions
uniq: Dict[Tuple[str, str, str, str], Endpoint] = {}
for endpoint in all_endpoints:
key = (endpoint.method, endpoint.path, endpoint.handler_file, endpoint.handler_name)
uniq[key] = endpoint
ordered = sorted(
enrich_endpoints(list(uniq.values()), file_function_map, dto_fields),
key=lambda e: (e.path, e.method, e.handler_file, e.handler_name),
)
write_reference(ordered)
write_catalog(ordered)
print(
f"Wrote {REFERENCE_OUTPUT} and {CATALOG_OUTPUT} ({len(ordered)} endpoints)"
)
if __name__ == "__main__":
main()