Remove generated API reference tooling and keep manual catalog
All checks were successful
build-and-deploy / build-deploy (push) Successful in 17s
All checks were successful
build-and-deploy / build-deploy (push) Successful in 17s
This commit is contained in:
1764
docs/API_CATALOG.md
1764
docs/API_CATALOG.md
File diff suppressed because it is too large
Load Diff
@@ -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`) |
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user