Skip to main content

Overview

Order management Cloud Functions handle the complete order lifecycle from creation through fulfillment. Each payment method creates orders using a standardized structure in the orders collection.

Order Creation Flow

All payment methods follow a consistent order creation pattern:
1

Validate User Authentication

Verify Firebase ID token or auth context
2

Fetch Real Product Prices

Query Firestore products collection to prevent price tampering
3

Create Order Document

Save order to orders/{orderId} with status PENDIENTE_PAGO or PENDIENTE
4

Return Payment Link

For online payments, return checkout URL; for COD, return order ID

Order Document Schema

All orders share this common structure:
{
  // Metadata
  source: 'TIENDA_WEB',
  createdAt: Timestamp,
  updatedAt: Timestamp,
  
  // User Information
  userId: 'firebase-uid',
  userEmail: '[email protected]',
  userName: 'Juan Pérez',
  phone: '3001234567',
  clientDoc: '1234567890',
  
  // Shipping & Billing
  shippingData: {
    address: 'Calle 123 #45-67',
    city: 'Bogotá',
    department: 'Cundinamarca',
    phone: '3001234567'
  },
  billingData: { /* optional */ },
  requiresInvoice: false,
  
  // Order Items
  items: [
    {
      id: 'product-id',
      name: 'iPhone 14 Pro',
      price: 4500000,
      quantity: 1,
      color: 'negro',
      capacity: '256GB',
      mainImage: 'https://...'
    }
  ],
  
  // Pricing
  subtotal: 4500000,
  shippingCost: 15000,
  total: 4515000,
  
  // Status Management
  status: 'PENDIENTE_PAGO',     // or PAGADO, DESPACHADO, ENTREGADO, CANCELADO
  paymentStatus: 'PENDING',      // or PAID, REJECTED
  paymentMethod: 'MERCADOPAGO',  // or ADDI, SISTECREDITO, CONTRAENTREGA
  
  // Inventory Control
  isStockDeducted: false,
  
  // Payment Details (set by webhook)
  paymentId: 'mp-123456',
  paymentAccountId: 'account-xyz',
  paymentMethodName: 'MercadoPago',
  
  // Shipping (set when dispatched)
  carrier: 'Servientrega',
  trackingNumber: '1234567890',
  
  // Additional
  buyerInfo: { /* legacy field */ },
  notes: 'Customer notes'
}

Order Status Workflow

Status Definitions

PENDIENTE_PAGO
string
Order created, awaiting online payment confirmation. Stock NOT deducted.
PENDIENTE
string
Cash on Delivery order confirmed. Stock IS deducted immediately.
PAGADO
string
Payment confirmed. Stock deducted. Order ready for fulfillment.
DESPACHADO
string
Order shipped with carrier. Tracking number assigned.
ENTREGADO
string
Customer received the order successfully.
RECHAZADO
string
Payment failed or rejected by gateway.
CANCELADO
string
Order cancelled by system (expired) or admin.

Inventory Management

Stock Deduction Logic

From mercadopago.js:213-239:
if (!oData.isStockDeducted) {
    for(const i of oData.items) {
        const pRef = db.collection('products').doc(i.id);
        const pDoc = await t.get(pRef);
        const pData = pDoc.data();
        
        // Deduct main stock
        let newStock = (pData.stock || 0) - (i.quantity || 1);
        
        // Handle product variants
        let newCombinations = pData.combinations || [];
        if (i.color || i.capacity) {
            const idx = newCombinations.findIndex(c => 
                (c.color === i.color || !c.color) &&
                (c.capacity === i.capacity || !c.capacity)
            );
            if (idx >= 0) {
                newCombinations[idx].stock -= i.quantity;
            }
        }
        
        t.update(pRef, { 
            stock: Math.max(0, newStock), 
            combinations: newCombinations 
        });
    }
}
Variant Stock: Products with color/capacity combinations have separate stock tracking in the combinations array.

Stock Validation (COD)

Cash on Delivery orders validate stock availability before creation:
for (const item of rawItems) {
    const pDoc = await t.get(pRef);
    let newStock = (pData.stock || 0) - qty;
    
    if (newStock < 0) {
        throw new Error(`Sin stock: ${pData.name}`);
    }
}

Order Update Functions

Payment Confirmation (Webhooks)

When a payment is approved, webhooks perform these actions atomically: From mercadopago.js:204-305:
1

Verify Not Already Paid

Check paymentStatus !== 'PAID' to prevent duplicate processing
2

Deduct Inventory

Update product stock and variant combinations
3

Credit Treasury Account

Add payment to configured gateway account (e.g., MercadoPago account)
4

Create Income Record

Log transaction in expenses collection with type INCOME
5

Generate Remission

Create fulfillment document in remissions collection
6

Update Order Status

Set status to PAGADO, paymentStatus to PAID, mark isStockDeducted: true
Transaction Example:
await db.runTransaction(async (t) => {
    const orderSnap = await t.get(orderRef);
    const oData = orderSnap.data();
    
    // Prevent duplicate processing
    if (oData.paymentStatus === 'PAID') return;
    
    // 1. Update inventory (see above)
    // 2. Update treasury
    const accSnap = await t.get(
        db.collection('accounts')
          .where('gatewayLink', '==', 'MERCADOPAGO')
          .limit(1)
    );
    const accDoc = accSnap.docs[0];
    t.update(accDoc.ref, {
        balance: (accDoc.data().balance || 0) + oData.total
    });
    
    // 3. Create income record
    const incRef = db.collection('expenses').doc();
    t.set(incRef, {
        amount: oData.total,
        category: "Ingreso Ventas Online",
        description: `Venta MP #${orderId.slice(0,8)}`,
        paymentMethod: accDoc.data().name,
        type: 'INCOME',
        orderId: orderId,
        date: admin.firestore.FieldValue.serverTimestamp()
    });
    
    // 4. Create remission
    const remRef = db.collection('remissions').doc(orderId);
    t.set(remRef, {
        orderId,
        source: 'WEBHOOK_MP',
        items: oData.items,
        clientName: oData.userName,
        clientAddress: `${oData.shippingData.address}, ${oData.shippingData.city}`,
        total: oData.total,
        status: 'PENDIENTE_ALISTAMIENTO',
        type: 'VENTA_WEB',
        createdAt: admin.firestore.FieldValue.serverTimestamp()
    });
    
    // 5. Mark order as paid
    t.update(orderRef, {
        status: 'PAGADO',
        paymentStatus: 'PAID',
        paymentId: paymentId,
        isStockDeducted: true,
        updatedAt: admin.firestore.FieldValue.serverTimestamp()
    });
});

Treasury Integration

Account Linking

Each payment gateway is linked to a treasury account via the gatewayLink field:
// Query for MercadoPago account
const accQuery = db.collection('accounts')
    .where('gatewayLink', '==', 'MERCADOPAGO')
    .limit(1);

// Fallback to default online account
if (accQuery.empty) {
    accQuery = db.collection('accounts')
        .where('isDefaultOnline', '==', true)
        .limit(1);
}

Income Recording

All confirmed payments create an income record:
{
    amount: 4515000,
    category: "Ingreso Ventas Online",
    description: "Venta MP #ABC12345",
    paymentMethod: "MercadoPago",
    date: Timestamp,
    createdAt: Timestamp,
    type: 'INCOME',
    orderId: 'order-id',
    supplierName: 'Juan Pérez'
}

Remission Generation

When an order is paid, a remission (fulfillment document) is created: From mercadopago.js:286-293:
const remRef = db.collection('remissions').doc(orderId);
t.set(remRef, {
    orderId: orderId,
    source: 'WEBHOOK_MP',
    items: oData.items,
    clientName: oData.userName,
    clientPhone: oData.phone,
    clientDoc: oData.clientDoc,
    clientAddress: `${oData.shippingData.address}, ${oData.shippingData.city}`,
    total: oData.total,
    status: 'PENDIENTE_ALISTAMIENTO',
    type: 'VENTA_WEB',
    createdAt: admin.firestore.FieldValue.serverTimestamp()
});
Remissions use the same document ID as the order for easy reference.

Order Cancellation

Automatic Expiration

The cancelAbandonedPayments scheduler cancels unpaid orders after 35 minutes. From scheduler.js:157-211:
exports.cancelAbandonedPayments = onSchedule({
    schedule: "every 15 minutes"
}, async (event) => {
    const timeout = new Date();
    timeout.setMinutes(timeout.getMinutes() - 35);
    
    const snapshot = await db.collection('orders')
        .where('status', '==', 'PENDIENTE_PAGO')
        .where('createdAt', '<=', admin.firestore.Timestamp.fromDate(timeout))
        .get();
    
    const batch = db.batch();
    snapshot.docs.forEach(doc => {
        if (doc.data().paymentStatus !== 'PAID') {
            batch.update(doc.ref, {
                status: 'CANCELADO',
                statusDetail: 'expired_by_system',
                notes: "[Sistema: Cancelado por inactividad de pago]"
            });
        }
    });
    
    await batch.commit();
});

Manual Cancellation

Admins can manually cancel orders through the admin panel, which should:
  1. Update order status to CANCELADO
  2. Restore inventory if isStockDeducted === true
  3. Reverse treasury account balance if payment was received

Order Queries

Get User’s Orders

const userOrders = await db.collection('orders')
    .where('userId', '==', userId)
    .orderBy('createdAt', 'desc')
    .get();

Get Pending Orders

const pending = await db.collection('orders')
    .where('status', 'in', ['PENDIENTE', 'PAGADO'])
    .orderBy('createdAt', 'asc')
    .get();

Get Orders by Payment Method

const mpOrders = await db.collection('orders')
    .where('paymentMethod', '==', 'MERCADOPAGO')
    .where('paymentStatus', '==', 'PAID')
    .get();

Error Handling

Transaction Failures

try {
    await db.runTransaction(async (t) => {
        // Order processing logic
    });
} catch (error) {
    console.error("❌ Order Transaction Failed:", error);
    throw new functions.https.HttpsError('internal', error.message);
}

Stock Validation Errors

if (newStock < 0) {
    throw new Error(`Sin stock: ${pData.name}`);
}

Best Practices

Always Use Transactions

Use Firestore transactions for inventory and payment operations to ensure consistency

Validate Prices Server-Side

Never trust client-sent prices; always fetch from Firestore

Check isStockDeducted

Prevent duplicate inventory deductions in webhook handlers

Log All State Changes

Use updatedAt timestamps and status change logging for audit trails

Build docs developers (and LLMs) love