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 treats inventory as a first-class concern. Rather than requiring manual stock adjustments after each sale, the system hooks into the order lifecycle and deducts quantities automatically whenever an order enters the EN_PREPARACION stage. Stock levels are stored in both Firebase Realtime Database and a local Room table, kept consistent by InventorySyncService and the offline SyncManager.

The Inventory Data Class

data class Inventory(
    val productId: String,
    val productName: String,
    val currentStock: Double,
    val unitOfMeasure: String,
    val minimumStock: Double = 0.0,
    val category: String? = null,
    val version: Long = 0
)

productId

Matches the Product.id. The same key is used as the Firebase node key under the inventory/ collection and as the @PrimaryKey in InventoryEntity.

currentStock / minimumStock

Both are Double to support fractional units (e.g. 0.5 kg of cheese). When currentStock ≤ minimumStock, the item is considered low-stock.

unitOfMeasure

A free-text field (e.g. "kg", "unidades", "litros") displayed in admin screens and notification messages.

The Product Fields That Drive Inventory

Not every product tracks stock. The Product data class has two fields that control inventory behavior:
data class Product(
    val id: String = "",
    val name: String = "",
    val description: String = "",
    val category: String = "",
    val salePrice: Double? = null,
    val costPrice: Double? = null,
    val trackInventory: Boolean = false,   // Controls whether this product deducts stock
    val stock: Double = 0.0,               // Initial/current stock level on the product
    val minStock: Double = 0.0,            // Minimum threshold for low-stock alerts
    val imageUrl: String? = null,
    val isActive: Boolean = true,
    val createdAt: Long = System.currentTimeMillis(),
    val updatedAt: Long = System.currentTimeMillis(),
    val version: Long = 0
)
When trackInventory = true, the product is enrolled in the inventory system. Its stock value seeds the Inventory.currentStock when first synced, and minStock maps to Inventory.minimumStock.
When an OrderItem is constructed from a Product, the trackInventory flag is copied into the item. This means the inventory deduction logic reads OrderItem.trackInventory at order-confirmation time — it does not re-query the product catalog.

How Stock Deduction Works

Stock is deducted when the order status transitions to EN_PREPARACION. For every OrderItem in the order where trackInventory = true, the system calls FirebaseInventoryRepository.updateStock(productId, newQuantity) with the reduced amount. FirebaseInventoryRepositoryImpl handles the update as a partial field patch, writing only currentStock to avoid overwriting other inventory metadata:
override suspend fun updateStock(productId: String, newQuantity: Double) {
    val updates = mapOf("currentStock" to newQuantity)
    inventoryRef.child(productId).updateChildren(updates).await()
}
The helper adjustStock in FirebaseInventoryRepositoryImpl performs the read-then-write cycle, allowing callers to pass a negative quantity for deductions:
suspend fun adjustStock(productId: String, quantity: Double) {
    val currentStock = getCurrentStock(productId)
    val newStock = currentStock + quantity  // Pass negative quantity to deduct
    if (newStock < 0) throw IllegalArgumentException("Stock no puede ser negativo")
    updateStock(productId, newStock)
}

InventorySyncService

InventorySyncService is a @Singleton that bridges the product catalog and the inventory collection. It runs as a coroutine listener in the background and reacts to real-time product changes.
@Singleton
class InventorySyncService @Inject constructor(
    private val productRepository: FirebaseProductRepository,
    private val inventoryRepository: FirebaseInventoryRepository
) {
    suspend fun startInventorySync() {
        productRepository.getProductsRealTime()
            .distinctUntilChanged()
            .collect { products ->
                syncProductsToInventory(products)
            }
    }

    private suspend fun syncProductsToInventory(products: List<Product>) {
        val productsWithInventory = products.filter { it.trackInventory && it.isActive }
        productsWithInventory.forEach { product -> syncProductToInventory(product) }
        cleanupOrphanedInventory(productsWithInventory)
    }

    private suspend fun syncProductToInventory(product: Product) {
        val existingStock = inventoryRepository.getCurrentStock(product.id)
        if (existingStock == 0.0) {
            inventoryRepository.updateStock(product.id, product.stock)
        }
    }
}
1

Listen to product changes

startInventorySync() opens a real-time listener on the products/ Firebase node via getProductsRealTime(). distinctUntilChanged() prevents redundant processing when unrelated fields change.
2

Filter trackable products

Only products where trackInventory = true AND isActive = true are enrolled in inventory management.
3

Seed missing inventory entries

If a product has no inventory record yet (getCurrentStock returns 0.0), syncProductToInventory creates one seeded with product.stock.
4

Clean up orphaned entries

cleanupOrphanedInventory compares the set of valid product IDs against all inventory nodes. Entries without a matching active product have their stock zeroed out, preventing phantom inventory from accumulating.
5

Consistency check

checkInventoryConsistency() can be called on demand; it re-seeds any inventory entry whose currentStock has drifted to 0.0 when the product still shows a positive stock value.

Low-Stock Detection and AdminStockWorker

Periodic low-stock checks are run by AdminStockWorker, a HiltWorker that reads directly from the local Room database to avoid requiring network connectivity.
@HiltWorker
class AdminStockWorker @AssistedInject constructor(
    @Assisted private val appContext: Context,
    @Assisted params: WorkerParameters,
    private val db: AppDatabase
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        return withContext(Dispatchers.IO) {
            val products = db.productDao().getAll()
            val trackedProducts = products.filter { it.trackInventory }

            val outOfStock = trackedProducts.filter { it.stock == 0.0 }
            val lowStock = trackedProducts.filter { it.stock > 0 && it.stock <= it.minStock }

            if (outOfStock.isNotEmpty() || lowStock.isNotEmpty()) {
                sendNotification(outOfStock, lowStock)
            }
            Result.success()
        }
    }
}
The worker distinguishes two severity levels:
ConditionLabelNotification color
stock == 0.0AGOTADOR.color.notification_out_of_stock (red)
stock > 0 && stock <= minStockStock bajoR.color.notification_low_stock (amber)
The notification is posted to the admin_stock_channel channel with IMPORTANCE_HIGH and includes a PendingIntent that deep-links directly to the inventory screen inside MainActivity.

InventoryDao Queries

@Dao
interface InventoryDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(item: InventoryEntity)

    @Query("SELECT * FROM inventory")
    suspend fun getAll(): List<InventoryEntity>

    @Query("SELECT * FROM inventory WHERE syncStatus = 'PENDING'")
    suspend fun getPending(): List<InventoryEntity>

    @Query("UPDATE inventory SET syncStatus = :status WHERE productId = :id")
    suspend fun updateStatus(id: String, status: String)

    @Query("UPDATE inventory SET syncStatus = :status, version = :newVersion, lastModified = :lastModified WHERE productId = :id AND version < :newVersion")
    suspend fun updateStatusIfNewer(id: String, status: String, newVersion: Long, lastModified: Long)

    @Query("SELECT * FROM inventory WHERE productId = :id")
    suspend fun getById(id: String): InventoryEntity?

    @Query("DELETE FROM inventory WHERE productId = :productId")
    suspend fun deleteProduct(productId: String)
}
updateStatusIfNewer uses a conditional WHERE version < :newVersion guard. This prevents a stale Firebase push from overwriting a more recent local edit that hasn’t been uploaded yet.

Local Entity: InventoryEntity

@Entity(tableName = "inventory")
data class InventoryEntity(
    @PrimaryKey val productId: String,
    val productName: String,
    val currentStock: Double,
    val unitOfMeasure: String,
    val minimumStock: Double,
    val category: String?,
    val syncStatus: String = "PENDING",
    val version: Long = System.currentTimeMillis(),
    val lastModified: Long = System.currentTimeMillis()
)
syncStatus mirrors the same two-state pattern used by orders and tables: "PENDING" means the record has a local change that has not yet reached Firebase; "SYNCED" means the local and remote copies agree.

Build docs developers (and LLMs) love