Documentation Index
Fetch the complete documentation index at: https://mintlify.com/luisumit/LaPreviaRestobar/llms.txt
Use this file to discover all available pages before exploring further.
La Previa Restobar is structured around Clean Architecture: every dependency arrow points inward from Presentation through Domain to Data. The Domain layer holds the business rules and declares repository interfaces; the Data layer provides concrete implementations backed by Room and Firebase; and the Presentation layer builds composable screens driven by Hilt-injected ViewModels. This separation keeps all business logic independently testable and makes it straightforward to swap data sources without touching a single screen.
Layer overview
┌─────────────────────────────────────────────────────────────────┐
│ PRESENTATION │
│ Jetpack Compose screens + @HiltViewModel classes │
│ WaiterViewModel · ChefViewModel · AdminViewModel │
│ LoginViewModel · SharedViewModel · InventoryViewModel │
│ SyncViewModel │
└──────────────────────────┬──────────────────────────────────────┘
│ calls use cases / observes StateFlow
┌──────────────────────────▼──────────────────────────────────────┐
│ DOMAIN │
│ Use Cases (business logic, no Android imports) │
│ Repository interfaces (abstract contracts) │
│ Domain models: Order · Product · Table · Inventory · UserRole │
│ Services: FirebaseInitializerService · InventorySyncService │
│ Worker: SyncWorker │
└──────────────────────────┬──────────────────────────────────────┘
│ implements
┌──────────────────────────▼──────────────────────────────────────┐
│ DATA │
│ ┌───────────────────┐ ┌──────────────────────────────────┐ │
│ │ LOCAL (Room) │ │ REMOTE (Firebase + Retrofit) │ │
│ │ AppDatabase v7 │ │ FirebaseOrderRepositoryImpl │ │
│ │ OrderDao │ │ FirebaseProductRepositoryImpl │ │
│ │ ProductDao │ │ FirebaseTableRepositoryImpl │ │
│ │ TableDao │ │ FirebaseInventoryRepositoryImpl │ │
│ │ InventoryDao │ │ ApiService (Retrofit) │ │
│ └───────────────────┘ │ RealTimeWebSocketClient │ │
│ Unified repositories └──────────────────────────────────┘ │
│ Mappers (Entity ↔ Domain ↔ DTO) │
└─────────────────────────────────────────────────────────────────┘
Presentation layer
The Presentation layer contains all Jetpack Compose screens and the ViewModels that drive them. Screens are pure composables with no direct data-access logic; they observe StateFlow values exposed by their ViewModel and dispatch user actions as function calls.
Navigation
AppNavigation() is the single NavHost for the entire app. It uses rememberNavController() and reacts to LoginViewModel.currentUser and LoginViewModel.userRole via LaunchedEffect. Every role-specific route is protected — it checks isAuthenticated && userRole == <expected role> before rendering the screen content, and redirects to login if the check fails.
login
├── waiter_main (UserRole.MESERO)
│ ├── tables
│ ├── table_details/{tableId}
│ ├── orders
│ ├── products
│ └── inventory
├── chef_main (UserRole.COCINERO)
└── admin_main (UserRole.ADMIN)
ViewModels and their responsibilities
| ViewModel | Scope | Key responsibilities |
|---|
WaiterViewModel | Mesero screens | Tables & order management; createOrder(), markOrderAsDelivered(), markTableAsFree(), cancelOrder(); offline order creation with Room → Firebase sync; network-change monitoring with ConnectivityManager |
ChefViewModel | Chef screens | Real-time order intake via listenToNewOrders() and listenToOrderChanges(); status transitions acceptOrder(), startOrderPreparation(), markOrderAsReady(), completeOrder(); automatic inventory deduction on ACEPTADO |
AdminViewModel | Admin screens | Product CRUD (createProduct, updateProduct, deleteProduct); AdminUiState with SalesReport and AdminDashboardMetrics; PDF/CSV export via PdfDocument and MediaStore; AdminReportFilter (DAY/WEEK/MONTH/YEAR/CUSTOM) |
LoginViewModel | App-wide | Firebase Auth sign-in/sign-out; currentUser: StateFlow<FirebaseUser?>; userRole: StateFlow<UserRole?> read from UserPreferencesRepository |
SharedViewModel | App-wide | WebSocket connection lifecycle via RealTimeWebSocketClient; broadcasts orderUpdates, tableUpdates, productUpdates, and notifications as SharedFlow to role-specific ViewModels; periodic HTTP health-check |
InventoryViewModel | Chef + Admin | Inventory listing and stock-level monitoring; low-stock alert state |
SyncViewModel | App-wide | Exposes manual sync trigger; delegates to SyncManager |
WaiterViewModel and ChefViewModel each run their own ConnectivityManager.NetworkCallback so they can start syncing PENDING orders immediately when the network becomes available, without waiting for the hourly SyncWorker interval.
Domain layer
The Domain layer is the heart of the application. It contains:
Use cases
Each use case is a single-responsibility class injected via Hilt. Use cases depend only on abstract repository interfaces — never on Room, Firebase, or Retrofit classes directly.
| Use Case | Injected repositories | What it does |
|---|
CreateOrderUseCase | OrderRepository, TableRepository | Persists a new Order and calls assignOrderToTable() atomically |
GetProductsUseCase | ProductRepository | Returns a Flow<List<Product>> of available products |
GetTablesUseCase | TableRepository | Returns a Flow<List<Table>> reflecting current occupancy |
UpdateOrderStatusUseCase | OrderRepository | Validates and applies a status transition on an existing order |
CreateProductUseCase | ProductRepository | Validates and persists a new product to the catalogue |
UpdateProductUseCase | ProductRepository | Applies field-level updates to an existing product |
DeleteProductUseCase | ProductRepository | Soft- or hard-deletes a product and handles related inventory cleanup |
Repository interfaces
The Domain layer declares four Firebase-focused repository interfaces (in domain/repository/):
FirebaseOrderRepository — order creation, status updates, real-time listeners
FirebaseProductRepository — product CRUD, getProductStock(), updateProductStock(), listenToProductChanges()
FirebaseTableRepository — table state, initializeDefaultTables(), assignOrderToTable(), clearTable()
FirebaseInventoryRepository — stock level reads and updateStock()
And four complementary local-first interfaces (OrderRepository, ProductRepository, TableRepository, InventoryRepository) used by the use cases.
Domain services
FirebaseInitializerService bootstraps all Firebase nodes (tables, products, orders, inventory) on app start. InventorySyncService listens for product stock changes in Firebase and keeps the local Room cache consistent.
Data layer
Room — local persistence (AppDatabase)
AppDatabase (version 7, named restobar_db) contains four entities and their DAOs:
| Entity | DAO | Notable fields |
|---|
OrderEntity | OrderDao | id, tableId, tableNumber, items (JSON), status, syncStatus, updatedAt |
ProductEntity | ProductDao | id, name, category, salePrice, costPrice, stock, minStock, trackInventory, syncStatus, version, lastModified |
TableEntity | TableDao | id, number, status, currentOrderId, syncStatus |
InventoryEntity | InventoryDao | productId, currentStock, productName, category, syncStatus, version, lastModified |
The syncStatus column is the backbone of offline-first writes. Every local insert gets either "PENDING" (created offline) or "SYNCED" (confirmed by Firebase). OrderDao.getPending() and ProductDao.getPending() return the queues that SyncWorker and the per-ViewModel network callbacks drain.
DatabaseModule provides the singleton AppDatabase via Hilt:
// di/DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"restobar_db"
)
.fallbackToDestructiveMigration()
.build()
}
}
Firebase — remote data source
FirebaseModule configures Firebase with disk persistence enabled and provides scoped DatabaseReference qualifiers for each data node:
// di/FirebaseModule.kt
@Provides @Singleton
fun provideFirebaseDatabase(): FirebaseDatabase {
return FirebaseDatabase.getInstance().apply {
try { setPersistenceEnabled(true) } catch (e: Exception) { /* already set */ }
}
}
@Provides @Singleton @OrdersReference
fun provideOrdersReference(db: FirebaseDatabase): DatabaseReference =
db.getReference("orders")
@Provides @Singleton @TablesReference
fun provideTablesReference(db: FirebaseDatabase): DatabaseReference =
db.getReference("tables")
@Provides @Singleton @ProductsReference
fun provideProductsReference(db: FirebaseDatabase): DatabaseReference =
db.getReference("products")
@Provides @Singleton @InventoryReference
fun provideInventoryReference(db: FirebaseDatabase): DatabaseReference =
db.getReference("inventory")
Each Firebase repository implementation (FirebaseOrderRepositoryImpl, FirebaseProductRepositoryImpl, FirebaseTableRepositoryImpl, FirebaseInventoryRepositoryImpl) injects the appropriate @Qualifier-annotated DatabaseReference and wraps Firebase listeners in Kotlin callbackFlow builders to expose type-safe Flow streams.
Unified repositories
The Unified*Repository classes (UnifiedOrderRepository, UnifiedProductRepository, UnifiedTableRepository, UnifiedInventoryRepository) in data/repository/ coordinate the two data sources with a local-first emission strategy:
- Emit the current Room snapshot immediately so the UI has something to display.
- Collect the Firebase stream in parallel and upsert each remote record into Room with
syncStatus = "SYNCED".
- Re-emit the merged result (remote records + any remaining PENDING local records), deduplicated by
id.
// UnifiedOrderRepository.kt (excerpt)
fun getAllOrders(): Flow<List<Order>> = flow {
// 1. Emit local data immediately
val localOrders = localDao.getAll().map { it.toDomain() }
emit(localOrders)
// 2. Collect remote and merge
remoteRepo.getOrders().collect { remoteOrders ->
remoteOrders.forEach { order ->
localDao.insert(order.toEntity().copy(syncStatus = "SYNCED"))
}
val combined = remoteOrders + localDao.getPending().map { it.toDomain() }
emit(combined.distinctBy { it.id })
}
}
Offline-first sync flow
User action (create order / update product / etc.)
│
▼
Room.insert(entity.copy(syncStatus = "PENDING"))
│
┌──────▼──────┐
│ Internet? │
└──────┬──────┘
│ YES NO
▼ ▼
Firebase write Row stays PENDING in Room
│ │
Room.updateStatus │
("SYNCED") ┌──────────▼──────────────┐
│ SyncWorker runs (hourly │
│ or on network restored) │
│ via WorkManager │
└──────────┬───────────────┘
│
▼
SyncManager.syncLight()
reads getPending() rows
replays each to Firebase
marks rows "SYNCED"
WorkManager SyncWorker
SyncWorker is a @HiltWorker CoroutineWorker scheduled in LaPreviaApp.onCreate():
// SyncWorker.kt (schedule companion)
fun schedule(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
SYNC_INTERVAL_HOURS, TimeUnit.HOURS // 1 hour
).setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}
On each run, SyncWorker delegates to SyncManager.syncLight(). If the work throws, it retries up to 3 times with exponential backoff before returning Result.failure(). An scheduleImmediate() overload uses OneTimeWorkRequest for on-demand triggering from the SyncViewModel.
WorkManager is initialised on-demand (the AndroidManifest.xml removes the default WorkManagerInitializer startup entry) and is wired to Hilt via HiltWorkerFactory in LaPreviaApp:
// LaPreviaApp.kt
@HiltAndroidApp
class LaPreviaApp : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
Timestamp-based conflict resolution
ProductEntity and InventoryEntity carry version and lastModified (both Long epoch-milliseconds) fields. When SyncManager or a ViewModel upserts a remote record into Room, it compares updatedAt/lastModified timestamps before overwriting, preventing stale remote writes from clobbering fresher local edits.
Dependency injection with Hilt
All DI configuration lives in the di/ package. Each module is installed in SingletonComponent so provided objects are app-scoped singletons.
| Module | What it provides |
|---|
AppModule | PreferencesManager (DataStore), UserPreferencesRepository, ProductManager |
NetworkModule | OkHttpClient, Retrofit, ApiService, RealTimeWebSocketClient; per-variant @BaseUrl and @WebSocketUrl qualifiers |
DatabaseModule | AppDatabase (Room, restobar_db) |
FirebaseModule | FirebaseAuth, FirebaseDatabase, @OrdersReference / @TablesReference / @ProductsReference / @InventoryReference DatabaseReference qualifiers, FirebaseInitializerService, InventorySyncService |
RepositoryModule | @Binds mappings: FirebaseOrderRepositoryImpl → FirebaseOrderRepository, FirebaseProductRepositoryImpl → FirebaseProductRepository, FirebaseTableRepositoryImpl → FirebaseTableRepository, FirebaseInventoryRepositoryImpl → FirebaseInventoryRepository |
WorkerModule | Reserved di/ module file; SyncWorker is wired to Hilt via HiltWorkerFactory in LaPreviaApp (see WorkManager section above) |
UseCaseModule | Located in domain/usecase/; provides all seven use-case instances (CreateOrderUseCase, UpdateOrderStatusUseCase, CreateProductUseCase, GetProductsUseCase, UpdateProductUseCase, DeleteProductUseCase, GetTablesUseCase) |
NetworkModule uses a runtime emulator check to select the correct URL qualifier — http://10.0.2.2:8080/ for AVD emulators and http://192.168.0.104:8080/ for physical devices (in debug/staging builds), or https://api.laprevia.com/ for release.
Data flow: end-to-end order creation
The following sequence illustrates how all layers interact for a single Waiter → Chef order:
[WaiterViewModel.createOrder()]
│
├─ Room.insert(OrderEntity, syncStatus=PENDING/SYNCED)
├─ if online: FirebaseOrderRepository.createOrder(order)
│ FirebaseTableRepository.assignOrderToTable(tableId, orderId)
│ Room.updateStatus(orderId, "SYNCED")
└─ refreshOrdersFromRoom() → _orders.emit(activeOrders)
[Firebase Realtime Database] ←── new "orders/{id}" node written
│
└─ ChefViewModel.listenToNewOrders() collects emission
│
├─ Room.insert(OrderEntity, syncStatus=SYNCED)
├─ refreshOrdersFromRoom() → _orders.emit(activeOrders)
└─ showNewOrderNotification()
[Chef taps "Aceptar"]
│
└─ ChefViewModel.acceptOrder(orderId)
│
├─ Room.insert(entity.copy(status="ACEPTADO", syncStatus=PENDING))
├─ FirebaseOrderRepository.updateOrderStatus(orderId, "ACEPTADO")
├─ updateInventoryForOrder(orderId)
│ FirebaseProductRepository.updateProductStock(...)
│ FirebaseInventoryRepository.updateStock(...)
└─ Room.updateStatus(orderId, "SYNCED")
[WaiterViewModel.setupFirebaseRealtimeUpdates()]
└─ listenToOrderChanges() receives updated order
└─ showStatusChangeNotification() → _notifications.emit(ORDER_ACCEPTED)