Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProcesosAgilesUMSS/sansistore/llms.txt

Use this file to discover all available pages before exploring further.

The SansiStore shopping cart provides a persistent, real-time purchasing flow that works for both guest and authenticated buyers. Guest items are stored in localStorage; upon sign-in they are automatically synced to Firestore. The cart page at /carrito lists all items, shows live price and stock changes, allows quantity edits and item removal, and guides the buyer through a multi-step checkout ending in order creation.

Checkout Flow

1

View Cart

Navigate to /carrito. The <CartView> component loads itemsWithProducts from the CartContext, which reads the authenticated user’s users/{uid}/cartItems subcollection (or localStorage for guests) and enriches each item with live product and inventory data.
2

Review Items

Each <CartItemRow> shows the product image, name, unit price (with a strikethrough of the original price-at-add if the price changed), stock-aware quantity controls (+/−), and a remove button. The payment summary sidebar animates the running total with <AnimatedAmount>.
3

Confirm Order (Login Gate)

Clicking Confirmar pedido checks authentication. Unauthenticated users see <LoginModal> prompting them to sign in before continuing. Only users with the comprador role can proceed.
4

Select Delivery Location

<LocationSelectorModal> subscribes to users/{uid}/locations in real time via subscribeToUserLocations(). The buyer selects a saved address; if none exist, a link to /ubicaciones is shown to add one. The default location is pre-selected.
5

Confirm Payment

<PaymentConfirmModal> shows the selected location, the final total, and asks the buyer to confirm. The payment method is initialized as cash_on_delivery at order creation and updated later by the seller.
6

Order Created

createOrder() runs a Firestore batch write that atomically creates the orders document, all orderItems sub-documents, reserves stock in inventory, and creates a payments document. The cart is then cleared and <OrderSuccessModal> confirms the order.

Cart State Management

Cart state is managed with Nanostores (src/features/cart/store/cartStore.ts). Two atoms drive the cart badge in the navbar:
// src/features/cart/store/cartStore.ts
import { atom } from 'nanostores';

export const cartTotalUnits = atom<number>(0);  // total quantity across all items
export const cartAnimating = atom<boolean>(false); // triggers cart icon bounce

export function initCartStore() {
  if (typeof window === 'undefined') return;
  const items = getLocalCart();
  cartTotalUnits.set(getTotalUnits(items));
}

export function notifyCartUpdate(newTotal: number) {
  cartTotalUnits.set(newTotal);
  cartAnimating.set(true);
  setTimeout(() => cartAnimating.set(false), 800);
}
CartContext (src/features/cart/components/CartContext.tsx) provides the full cart state and actions to all React components below the <CartProvider> boundary:
const { addToCart, removeItem, updateQuantity, setQuantity,
        clearCart, items, itemsWithProducts, loading } = useCartContext();
<CartProvider> wraps both <FeaturedProducts> and <CartView>. On product pages it enables the quick-add button on every <ProductCard> and the Agregar al carrito button on the detail page.

Firestore Cart Structure

Cart items for authenticated users live in the users/{uid}/cartItems subcollection. Each document ID equals the productId.
// src/features/cart/types.ts
interface CartItem {
  cartItemId: string;   // same as productId
  userId: string;
  productId: string;
  quantity: number;
  updatedAt: Date;      // serverTimestamp() on every write
}

Key Firestore Operations

// src/features/cart/services/cartFirestore.ts
await upsertCartItem(uid, productId, quantity, priceAtAdd);
// uses setDoc(..., { merge: true }) — safe to call on new or existing items
When a user signs in, the local localStorage cart is merged with any existing Firestore cart and the result is written back to Firestore atomically.

Real-Time Price and Stock Checks

CartView uses getUserCartItems() which re-fetches the live products and inventory documents for each cart item on every load. It computes:
// src/features/cart/services/cartService.ts
const stockReserved = toPositiveStock(inventory?.stockReserved);
const stockAvailable = Math.max(
  0,
  toPositiveStock(inventory?.stockAvailable) - stockReserved
);

const availabilityMessage = getAvailabilityMessage({
  product, inventory, unitPrice, stockAvailable, quantity
});
const isAvailable = availabilityMessage === '';
The cart UI surfaces availability issues immediately:

Price Change

When the live price differs from priceAtAdd, a warning banner reads “N producto(s) cambió de precio. Revisa el total antes de confirmar.” The summary shows both the old (strikethrough) and new prices.

Out of Stock

When stockAvailable drops to zero between cart add and checkout, the row shows “Sin stock disponible.” and the Confirmar pedido button is disabled until the item is removed.

Product Inactive

If product.active === false or inventory.enabled === false, the availability message reads “El producto ya no está activo.” or “El producto no está disponible para venta.”

Invalid Price

If the computed unitPrice is 0 or negative, the message is “El producto no tiene un precio válido.”
Items marked unavailable are excluded from the order total but remain visible in the cart so the buyer can remove them manually. The Confirmar pedido button stays disabled while any invalidItems.length > 0.

Order Creation

createOrder() (src/features/cart/services/orderService.ts) performs all writes in a single Firestore writeBatch:
interface CreateOrderParams {
  user: User;
  selectedLocation: Location;
  total: number;
  includedItems: CartItemWithProduct[];
}

interface CreateOrderResult {
  orderId: string;      // UUID v4 + "_" + 6-char z-base-32 label
  orderSecret: string;  // 4-digit PIN for anonymous order lookup
  paymentCode: string;  // same as orderId; used as payments doc ID
}
The batch performs four types of writes atomically:
  1. orders/{orderId} — the order document (see field reference below)
  2. orders/{orderId}/orderItems/{itemId} — one document per included cart item
  3. inventory/{productId}stockReserved += quantity for each item
  4. payments/{paymentCode} — initial payment record with status: 'PENDIENTE'
// Order document written by createOrder()
{
  orderId,
  secret: orderSecret,        // 4-digit PIN for anonymous lookup
  buyerId: user.uid,
  sellerId: null,
  customerName: user.displayName || 'Cliente no registrado',
  customerPhone: user.phoneNumber || '',
  address: selectedLocation.label,
  status: 'CREADO',
  total,
  locationId: selectedLocation.id,
  paymentStatus: 'PENDIENTE',
  deliveryStatus: null,
  paymentId: paymentCode,
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp(),
}

Order ID Format

Order IDs are generated as {uuidv4}_{zBase32Label}:
// Note: uuidv7 is a local alias for UUID v4 (`import { v4 as uuidv7 } from 'uuid'`)
// Example: "019e74a5-808a-7321-9127-281f098b2d8e_3dg-zjs"
export function generateOrderId(): string {
  return `${uuidv7()}_${generateZBase32Label()}`;
}

// The friendly label (after "_") is shown in the UI
export function parseOrderId(orderId: string) {
  const idx = orderId.indexOf('_');
  return {
    uuid: orderId.slice(0, idx),
    friendlyName: orderId.slice(idx + 1), // e.g. "3dg-zjs"
  };
}
The secret field is a 4-digit shuffled PIN generated client-side, used to allow anonymous order status lookups without requiring sign-in.

Firestore Data Model

users/{uid}/cartItems subcollection

FieldTypeDescription
cartItemIdstringSame as productId (document ID)
userIdstringFirebase Auth UID of the cart owner
productIdstringReference to products collection
quantitynumberInteger ≥ 1
updatedAtTimestampserverTimestamp() on every upsert
priceAtAddnumber?Price captured when item was added; used for price-change detection

payments collection

FieldTypeDescription
paymentIdstringSame as orderId
orderIdstringReference to the order
amountnumberOrder total in Bolivianos
methodstringcash_on_delivery (default); seller updates to final method
statusstringPENDIENTEPAGADO after seller validation
registeredBystringBuyer’s uid
registeredAtTimestampCreation time
Payment methods recognized by the system include EFECTIVO (cash on delivery), QR, TRANSFERENCIA, and TARJETA. The method is recorded in the payments document when the seller validates payment.

End-to-End Tests

Cart behavior is covered by Playwright tests in tests/cart/cart.spec.ts:
Cart - Carrito
  ✓ should display cart items when user is authenticated
      — shows "Leche PIL Natural 900 ml" at Bs 9.70/u
      — shows "Pan Integral Bimbo (precio rebajado)" at Bs 15.00 → Bs 10.00/u
  ✓ should show empty cart message when user has no items
      — "Tu carrito está vacío" + "Comprar ahora" link
  ✓ should clear cart when user logs out
  ✓ should show empty cart after logout and login with different user
  ✓ should allow non-logged user to add items to cart
  ✓ should show updated price in real time before confirming
      — price change banner: "El precio subió de Bs 10.00 a Bs 12.50."
  ✓ should mark product unavailable in real time when stock reaches zero
      — "Sin stock disponible." + "Confirmar pedido" disabled
  ✓ should show image fallback in real time when product image breaks
  ✓ should clear local storage cart for guest/anonymous user
      — ClearCartModal confirm/cancel flow
  ✓ should clear Firestore cart for authenticated user
      — verifies users/{uid}/cartItems is empty via Admin SDK

Build docs developers (and LLMs) love