Skip to main content

Overview

The products collection stores the complete product catalog with support for color/capacity variants, pricing, inventory tracking, and promotional flags.

Collection Path

products/{productId}

Document Schema

Product Document
object

Example Document

{
  "name": "iPhone 14 Pro",
  "category": "Celulares",
  "subcategory": "Apple",
  "brand": "Apple",
  "description": "<p>iPhone con chip A16 Bionic y cámara de 48MP</p>",
  
  "price": 4500000,
  "originalPrice": 5000000,
  "cost": 3800000,
  "promoEndsAt": "2026-03-15T23:59:59Z",
  
  "stock": 15,
  "minStock": 3,
  "sku": "APL-IP14P-256",
  
  "mainImage": "https://storage.googleapis.com/pixeltech/iphone14pro.jpg",
  "images": [
    "https://storage.googleapis.com/pixeltech/iphone14pro-2.jpg",
    "https://storage.googleapis.com/pixeltech/iphone14pro-3.jpg"
  ],
  
  "hasVariants": true,
  "variants": [
    {
      "color": "negro",
      "images": ["https://.../negro-1.jpg"]
    },
    {
      "color": "plateado",
      "images": ["https://.../plateado-1.jpg"]
    }
  ],
  
  "hasCapacities": true,
  "capacities": [
    { "label": "128GB", "price": 4200000 },
    { "label": "256GB", "price": 4500000 },
    { "label": "512GB", "price": 5200000 }
  ],
  
  "combinations": [
    { "color": "negro", "capacity": "256GB", "price": 4500000, "stock": 8 },
    { "color": "plateado", "capacity": "256GB", "price": 4500000, "stock": 7 }
  ],
  
  "status": "active",
  "isWeeklyChoice": true,
  "isHeroPromo": false,
  "isNewLaunch": false,
  
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-03-05T14:22:00Z"
}

Queries

Get All Active Products

const snapshot = await db.collection('products')
    .where('status', '==', 'active')
    .get();

Get Products by Category

const snapshot = await db.collection('products')
    .where('status', '==', 'active')
    .where('category', '==', 'Celulares')
    .orderBy('name', 'asc')
    .get();
const snapshot = await db.collection('products')
    .where('isWeeklyChoice', '==', true)
    .where('status', '==', 'active')
    .get();

Get Low Stock Products

const snapshot = await db.collection('products')
    .where('stock', '<=', db.collection('products').doc().data().minStock)
    .get();

Get Updated Products (Delta Sync)

Used by SmartProductSync:
const lastSyncTime = new Date(1709647200000); // User's last visit

const snapshot = await db.collection('products')
    .where('updatedAt', '>', lastSyncTime)
    .get();

Variant Pricing Logic

When a product has both colors and capacities:
  1. Check combinations array first:
    const combo = product.combinations.find(c => 
        c.color === selectedColor && c.capacity === selectedCapacity
    );
    const price = combo ? combo.price : product.price;
    
  2. Fallback to capacities array:
    if (!combo && product.capacities) {
        const cap = product.capacities.find(c => c.label === selectedCapacity);
        const price = cap ? cap.price : product.price;
    }
    
  3. Default to base price:
    const price = product.price;
    

Stock Management

Deducting Inventory

When an order is paid, stock is deducted from both global and variant-specific stock:
// Global stock
let newStock = product.stock - orderItem.quantity;

// Variant stock (if applicable)
if (orderItem.color || orderItem.capacity) {
    const idx = product.combinations.findIndex(c =>
        c.color === orderItem.color && c.capacity === orderItem.capacity
    );
    if (idx >= 0) {
        product.combinations[idx].stock -= orderItem.quantity;
    }
}

await productRef.update({
    stock: newStock,
    combinations: product.combinations
});

Triggering Updates

Critical: Always update updatedAt when modifying products to trigger SmartProductSync delta updates.
await productRef.update({
    stock: newStock,
    updatedAt: admin.firestore.FieldValue.serverTimestamp()
});

Security Rules

Recommended Firestore security rules:
match /products/{productId} {
  // Allow public read for active products
  allow read: if resource.data.status == 'active';
  
  // Only admins can write
  allow write: if request.auth != null && 
               get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}

Indexes

Required composite indexes:
// Category + Status
products: [category ASC, status ASC]

// Updated timestamp (for delta sync)
products: [updatedAt DESC, status ASC]

// Promo flags
products: [isHeroPromo ASC, status ASC]
products: [isNewLaunch ASC, status ASC]
products: [promoEndsAt ASC]

Build docs developers (and LLMs) love