Skip to main content

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. 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

ViewModelScopeKey responsibilities
WaiterViewModelMesero screensTables & order management; createOrder(), markOrderAsDelivered(), markTableAsFree(), cancelOrder(); offline order creation with Room → Firebase sync; network-change monitoring with ConnectivityManager
ChefViewModelChef screensReal-time order intake via listenToNewOrders() and listenToOrderChanges(); status transitions acceptOrder(), startOrderPreparation(), markOrderAsReady(), completeOrder(); automatic inventory deduction on ACEPTADO
AdminViewModelAdmin screensProduct CRUD (createProduct, updateProduct, deleteProduct); AdminUiState with SalesReport and AdminDashboardMetrics; PDF/CSV export via PdfDocument and MediaStore; AdminReportFilter (DAY/WEEK/MONTH/YEAR/CUSTOM)
LoginViewModelApp-wideFirebase Auth sign-in/sign-out; currentUser: StateFlow<FirebaseUser?>; userRole: StateFlow<UserRole?> read from UserPreferencesRepository
SharedViewModelApp-wideWebSocket connection lifecycle via RealTimeWebSocketClient; broadcasts orderUpdates, tableUpdates, productUpdates, and notifications as SharedFlow to role-specific ViewModels; periodic HTTP health-check
InventoryViewModelChef + AdminInventory listing and stock-level monitoring; low-stock alert state
SyncViewModelApp-wideExposes 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 CaseInjected repositoriesWhat it does
CreateOrderUseCaseOrderRepository, TableRepositoryPersists a new Order and calls assignOrderToTable() atomically
GetProductsUseCaseProductRepositoryReturns a Flow<List<Product>> of available products
GetTablesUseCaseTableRepositoryReturns a Flow<List<Table>> reflecting current occupancy
UpdateOrderStatusUseCaseOrderRepositoryValidates and applies a status transition on an existing order
CreateProductUseCaseProductRepositoryValidates and persists a new product to the catalogue
UpdateProductUseCaseProductRepositoryApplies field-level updates to an existing product
DeleteProductUseCaseProductRepositorySoft- 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:
EntityDAONotable fields
OrderEntityOrderDaoid, tableId, tableNumber, items (JSON), status, syncStatus, updatedAt
ProductEntityProductDaoid, name, category, salePrice, costPrice, stock, minStock, trackInventory, syncStatus, version, lastModified
TableEntityTableDaoid, number, status, currentOrderId, syncStatus
InventoryEntityInventoryDaoproductId, 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:
  1. Emit the current Room snapshot immediately so the UI has something to display.
  2. Collect the Firebase stream in parallel and upsert each remote record into Room with syncStatus = "SYNCED".
  3. 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.
ModuleWhat it provides
AppModulePreferencesManager (DataStore), UserPreferencesRepository, ProductManager
NetworkModuleOkHttpClient, Retrofit, ApiService, RealTimeWebSocketClient; per-variant @BaseUrl and @WebSocketUrl qualifiers
DatabaseModuleAppDatabase (Room, restobar_db)
FirebaseModuleFirebaseAuth, FirebaseDatabase, @OrdersReference / @TablesReference / @ProductsReference / @InventoryReference DatabaseReference qualifiers, FirebaseInitializerService, InventorySyncService
RepositoryModule@Binds mappings: FirebaseOrderRepositoryImpl → FirebaseOrderRepository, FirebaseProductRepositoryImpl → FirebaseProductRepository, FirebaseTableRepositoryImpl → FirebaseTableRepository, FirebaseInventoryRepositoryImpl → FirebaseInventoryRepository
WorkerModuleReserved di/ module file; SyncWorker is wired to Hilt via HiltWorkerFactory in LaPreviaApp (see WorkManager section above)
UseCaseModuleLocated 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)

Build docs developers (and LLMs) love