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 designed to keep working even when the restaurant’s Wi-Fi is unreliable. Every write — whether a new order, a table status change, or a stock update — lands in the local Room database first. When the device reconnects to the network, WorkManager wakes up SyncWorker, which hands control to SyncManager to push all pending records to Firebase Realtime Database.

The Offline-First Pattern

Local action (user)


  Write to Room
  (syncStatus = "PENDING")


  WorkManager queued
  (waits for NETWORK_TYPE_CONNECTED)


  Network restored


  SyncWorker.doWork() fires


  SyncManager reads PENDING records


  Push to Firebase Realtime Database


  Mark Room record syncStatus = "SYNCED"
Every entity type follows this same pipeline. Nothing is lost if the device goes offline mid-shift — the pending queue simply grows until connectivity returns.

syncStatus Field

Orders, tables, and inventory items all carry a syncStatus string column in their Room entity:
ValueMeaning
"PENDING"Local change exists that has not yet been pushed to Firebase
"SYNCED"Local and remote records are in agreement
The field starts as "PENDING" for newly created or locally modified records, and is set to "SYNCED" by SyncManager after a successful Firebase write.
Table records default to "SYNCED" because the initial table list is downloaded from Firebase on first launch. Only tables modified offline (e.g. status change without network) flip to "PENDING".

SyncWorker

SyncWorker is a HiltWorker that runs on the IO dispatcher and delegates all sync logic to SyncManager.
@HiltWorker
class SyncWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val syncManager: SyncManager
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            try {
                syncManager.syncLight()   // Lightweight: syncs orders only
                Result.success()
            } catch (e: Exception) {
                if (runAttemptCount < 3) Result.retry()
                else Result.failure()
            }
        }
    }

    companion object {
        private const val WORK_NAME = "background_sync_work"
        private const val SYNC_INTERVAL_HOURS = 1L

        fun schedule(context: Context) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresBatteryNotLow(true)
                .build()

            val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
                SYNC_INTERVAL_HOURS, TimeUnit.HOURS
            ).setConstraints(constraints)
                .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES)
                .setInitialDelay(5, TimeUnit.MINUTES)
                .build()

            WorkManager.getInstance(context).enqueueUniquePeriodicWork(
                WORK_NAME,
                ExistingPeriodicWorkPolicy.KEEP,
                syncRequest
            )
        }

        fun scheduleImmediate(context: Context) {
            val immediateRequest = OneTimeWorkRequestBuilder<SyncWorker>()
                .setConstraints(
                    Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.CONNECTED)
                        .build()
                ).build()
            WorkManager.getInstance(context).enqueue(immediateRequest)
        }
    }
}

WorkManager Constraints

NETWORK_TYPE_CONNECTED ensures the worker only runs when a network interface is active. REQUIRES_BATTERY_NOT_LOW prevents sync tasks from draining a dying device.

Retry with back-off

If doWork() throws, WorkManager retries up to 3 times with BackoffPolicy.EXPONENTIAL starting at a 15-minute interval. After 3 failures, Result.failure() is returned and the work is discarded from the queue.

Periodic + immediate

schedule() registers a recurring hourly task. scheduleImmediate() enqueues a one-time task — called when the app detects connectivity restored after a period offline.

KEEP policy

ExistingPeriodicWorkPolicy.KEEP prevents duplicate workers accumulating if the app is launched multiple times or the scheduler is called again after a crash.

SyncManager

SyncManager is the @Singleton that knows how to read each Room DAO’s pending queue and push records to the corresponding Firebase repository.

Upload Flow (pending local → Firebase)

suspend fun syncOrders() = withContext(Dispatchers.IO) {
    val pending = db.orderDao().getPending()
    pending.forEach { entity ->
        try {
            firebaseOrders.createOrder(entity.toDomain())
            db.orderDao().updateStatus(entity.id, "SYNCED")
        } catch (e: Exception) {
            Timber.e(e, "❌ Order sync FAIL: %s", entity.id)
        }
    }
}
The same pattern is used for tables (syncTables), inventory (syncInventory), and products (syncProducts). Each method reads getPending(), pushes each record, and marks it "SYNCED" on success. A failure in one record does not block the rest — the try/catch is per-record, not per-batch.

Download Flow (Firebase → local Room)

suspend fun downloadOrders() = withContext(Dispatchers.IO) {
    val firebaseOrdersList = firebaseOrders.getOrders().first()

    firebaseOrdersList.forEach { remoteOrder ->
        val localOrder = db.orderDao().getById(remoteOrder.id)
        if (localOrder == null || remoteOrder.updatedAt > localOrder.updatedAt) {
            db.orderDao().insert(remoteOrder.toEntity().copy(syncStatus = "SYNCED"))
        }
    }
}
The download path applies timestamp-based conflict resolution before writing: a remote record only overwrites a local one if the remote updatedAt is strictly greater than the local updatedAt. This prevents a stale Firebase push from silently reverting a more recent offline edit.

Sync Modes

MethodWhat it doesTimeout
syncLight()Uploads pending orders only15 s
uploadAll()Uploads orders + tables + inventory + products30 s
downloadAll()Downloads all entity types from Firebase30 s
syncFull()uploadAll() then downloadAll()60 s
syncLight() is used by SyncWorker for routine background sync to minimize battery and data usage. The full sync variants are called explicitly after login or when a manual refresh is triggered.

Timestamp-Based Conflict Resolution

Every entity that participates in offline sync carries at least an updatedAt (domain model) and a lastModified (Room entity) timestamp. The conflict resolution rule is simple:
The record with the higher updatedAt timestamp wins.
This means:
  • If a waiter updates an order’s status while offline, that change has a newer updatedAt than the Firebase copy.
  • When sync runs, downloadOrders() skips overwriting the local record because remoteOrder.updatedAt is older.
  • If two devices edit the same record simultaneously, the last write (highest timestamp) prevails after both sync.
The Room DAOs reinforce this at the database level with the updateStatusIfNewer query:
@Query("""
  UPDATE orders
  SET syncStatus = :status, version = :newVersion, lastModified = :lastModified
  WHERE id = :id AND version < :newVersion
""")
suspend fun updateStatusIfNewer(
    id: String,
    status: String,
    newVersion: Long,
    lastModified: Long
)
The WHERE version < :newVersion clause is a database-level guard: even if application code passes an outdated version number, the SQL statement will silently no-op rather than overwrite a newer record.

Network Connectivity Detection

RealTimeWebSocketClient uses Android’s ConnectivityManager to check for an active network before attempting a WebSocket connection:
private fun isNetworkAvailable(): Boolean {
    val connectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val network = connectivityManager.activeNetwork ?: return false
    val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false
    return when {
        activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)     -> true
        activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
        activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
        else -> false
    }
}
This check covers Wi-Fi, mobile data, and Ethernet. If no transport is available, the client emits a WebSocketEvent.Error and schedules a reconnection attempt with exponential back-off (maximum delay: 30 seconds).

End-to-End Sync Flow

┌─────────────┐     write      ┌──────────────┐
│  User action │ ─────────────► │  Room (local)│
│  (any role)  │                │  syncStatus  │
└─────────────┘                │  = "PENDING" │
                                └──────┬───────┘

                              WorkManager queued
                              (NETWORK_TYPE_CONNECTED)

                                Network restored


                               ┌───────────────┐
                               │  SyncWorker   │
                               │  .doWork()    │
                               └──────┬────────┘
                                      │ calls

                               ┌───────────────┐
                               │  SyncManager  │
                               │  .syncLight() │  or syncFull()
                               └──────┬────────┘

                          reads getPending() from DAO


                          ┌───────────────────────┐
                          │  Firebase Realtime DB  │
                          │  createOrder /         │
                          │  updateTable /         │
                          │  updateInventory       │
                          └──────────┬────────────┘
                                     │ success

                          updateStatus(id, "SYNCED")
                          in Room DAO
Call SyncWorker.scheduleImmediate(context) from a ConnectivityManager network callback to trigger an instant sync the moment connectivity returns, rather than waiting for the next hourly window.

Build docs developers (and LLMs) love