diff --git a/AGENTS.md b/AGENTS.md index 6a47324..cfda38e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -527,3 +527,59 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance ## 7) Compose Notes - Use `androidx.compose.foundation.text.KeyboardOptions` for keyboard options imports. + +--- + +## 8) Engineering Structure & Anti-Boilerplate Rules + +### Non-negotiable coding rules + +- Never add duplicate business logic in multiple files. +- Never copy-paste permission checks, navigation decisions, mapping logic, or API call patterns. +- If similar logic appears 2+ times, extract shared function/class immediately. +- Prefer typed models/enums over raw strings for roles/status/flags. +- Keep files small and purpose-driven; split before a file becomes hard to scan. + +### Required project structure (current baseline) + +- `core/` -> cross-cutting business primitives/policies (e.g., auth policy, role enum). +- `data/api/core/` -> API client, constants, token providers, aggregated API service. +- `data/api/service/` -> Retrofit endpoint interfaces only. +- `data/api/model/` -> DTO/request/response models. +- `ui/navigation/` -> route model, navigation orchestrators, back-navigation rules. +- `ui//` -> screen + state + viewmodel for that feature. + +### How to implement future logic (mandatory workflow) + +1. Define/extend domain type first (enum/data model/policy) instead of raw literals. +2. Add/extend API contract in `data/api/service` and models in `data/api/model`. +3. Add shared logic once (policy/helper/mapper) in `core` or feature-common layer. +4. Keep ViewModel thin: orchestrate calls, state, and errors only. +5. Keep UI dumb: consume state and callbacks; avoid business rules in composables. +6. If navigation changes, update `ui/navigation` only (single source of truth). +7. Before finishing, remove any newly introduced duplication and compile-check. + +### PR/refactor acceptance checklist + +- No repeated role/permission checks across screens. +- No repeated model mapping blocks (extract mapper/helper). +- No giant god-file when it can be split by domain responsibility. +- Imports/packages follow the structure above. +- Build passes: `./gradlew :app:compileDebugKotlin`. + +### Guest Documents Authorization (mandatory) + +- View access: `ADMIN`, `MANAGER` (and super admin). +- Modify access (upload/delete): allowed only when booking status is `OPEN` or `CHECKED_IN`. +- For `CHECKED_OUT`, `CANCELLED`, `NO_SHOW`: documents are read-only. +- Never couple guest document permissions with Razorpay/settings permissions. + +### Permission design guardrail + +- Do not reuse one feature's permission gate for another unrelated feature. +- Add explicit policy methods in `core/auth/AuthzPolicy` for each feature capability. + +### Refactor safety rule + +- Any package/file movement must include import updates in same change. +- After refactor, compile check is mandatory: `./gradlew :app:compileDebugKotlin`. diff --git a/app/src/main/java/com/android/trisolarispms/core/auth/AuthzPolicy.kt b/app/src/main/java/com/android/trisolarispms/core/auth/AuthzPolicy.kt index b7c8fe4..8633e24 100644 --- a/app/src/main/java/com/android/trisolarispms/core/auth/AuthzPolicy.kt +++ b/app/src/main/java/com/android/trisolarispms/core/auth/AuthzPolicy.kt @@ -26,6 +26,9 @@ class AuthzPolicy( fun canRefundBookingPayment(propertyId: String): Boolean = hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER) + fun canManageGuestDocuments(propertyId: String): Boolean = + hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER) + fun canManagePropertyUsers(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN) fun canCreateBookingFor(propertyId: String): Boolean = diff --git a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt index 528edb1..6d4b761 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guestdocs/GuestDocumentsTab.kt @@ -71,6 +71,7 @@ fun GuestDocumentsTab( guestId: String, bookingId: String, canManageDocuments: Boolean, + canModifyDocuments: Boolean, viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId") ) { val state by viewModel.state.collectAsState() @@ -164,6 +165,13 @@ fun GuestDocumentsTab( Text(text = "You don't have access to view documents.") return@Column } + if (!canModifyDocuments) { + Text( + text = "Read-only: documents can be modified only when booking is OPEN or CHECKED_IN.", + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.height(8.dp)) + } if (!state.isLoading && state.documents.isEmpty()) { Text(text = "No documents yet") } @@ -190,7 +198,7 @@ fun GuestDocumentsTab( guestId = guestId, doc = doc, imageLoader = imageLoader, - canDelete = canManageDocuments, + canDelete = canModifyDocuments, onDelete = { documentId -> viewModel.deleteDocument(propertyId, guestId, documentId) } @@ -199,7 +207,7 @@ fun GuestDocumentsTab( } } - if (canManageDocuments) { + if (canModifyDocuments) { FloatingActionButton( onClick = { showPicker.value = true }, modifier = Modifier @@ -214,7 +222,7 @@ fun GuestDocumentsTab( } } - if (showPicker.value) { + if (showPicker.value && canModifyDocuments) { AlertDialog( onDismissRequest = { showPicker.value = false }, title = { Text("Add document") }, diff --git a/app/src/main/java/com/android/trisolarispms/ui/navigation/MainRoutesBooking.kt b/app/src/main/java/com/android/trisolarispms/ui/navigation/MainRoutesBooking.kt index 80a7a8b..cd4d66f 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/navigation/MainRoutesBooking.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/navigation/MainRoutesBooking.kt @@ -65,7 +65,7 @@ internal fun renderBookingRoutes( bookingId = currentRoute.bookingId ) }, - canManageDocuments = authz.canManageRazorpaySettings(currentRoute.propertyId) + canManageDocuments = authz.canManageGuestDocuments(currentRoute.propertyId) ) is AppRoute.BookingPayments -> BookingPaymentsScreen( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt index 87d4387..69ebc41 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/BookingDetailsTabsScreen.kt @@ -87,6 +87,10 @@ fun BookingDetailsTabsScreen( val scope = rememberCoroutineScope() val staysState by staysViewModel.state.collectAsState() val detailsState by detailsViewModel.state.collectAsState() + val canModifyDocuments = canManageDocuments && when (detailsState.details?.status) { + "OPEN", "CHECKED_IN" -> true + else -> false + } LaunchedEffect(propertyId, bookingId, guestId) { staysViewModel.load(propertyId, bookingId) @@ -164,7 +168,8 @@ fun BookingDetailsTabsScreen( propertyId = propertyId, guestId = resolvedGuestId, bookingId = bookingId, - canManageDocuments = canManageDocuments + canManageDocuments = canManageDocuments, + canModifyDocuments = canModifyDocuments ) } else { Box(