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.

SansiStore gives sellers a self-service promotions tool on the /seller/offers page. A seller selects any active product, sets a discount percentage, and defines a validity window. The platform computes the final offer price automatically, persists it to Firestore, and immediately reflects it in the buyer-facing catalog — no admin approval needed.

How Offers Work

When a seller creates an offer, two things happen atomically:
  1. A new document is added to the offers collection with the discount percentage, date range, and status.
  2. The corresponding product document in products/{productId} is updated with hasOffer: true, a computed offerPrice, and a badge label of "Oferta".
The DiscountBadge component uses these two product fields to render the price display for buyers: the original price is struck through and the offerPrice is shown prominently.

Offers Page — /seller/offers

Activate Offer

Select any active product from the dropdown. The list is sorted alphabetically and loaded via getProductsService, which queries products where active == true.

Set Offer Price

Enter a discount percentage between 1 and 100. The form computes offerPrice = price × (1 − discount / 100) and shows a live preview using DiscountBadge before the seller saves.

Set Validity Window

Choose a start date and an end date. The form validates that the end date is not before the start date. The status field is set to 'active' at creation time.

Offer in Catalog

Once saved, buyers see the offer price in the product catalog. The original price appears with a strikethrough and a red animated -X% Oferta badge is shown alongside the discounted price.

Offer Service

The entire offer creation flow is handled by createOfferService in src/features/offers/services/offerService.ts:
// src/features/offers/services/offerService.ts
export interface OfferData {
  productId: string;
  discount: number;       // percentage, 1–100
  startDate: string;      // ISO date string, e.g. "2025-06-01"
  endDate: string;        // ISO date string, e.g. "2025-06-30"
  status: 'active' | 'scheduled' | 'expired';
}

export const createOfferService = async (
  data: OfferData,
  originalPrice: number,
) => {
  try {
    // 1. Write to the `offers` collection
    const docRef = await addDoc(collection(db, 'offers'), {
      productId: data.productId,
      discount: data.discount,
      startDate: data.startDate,
      endDate: data.endDate,
      status: data.status,
      createdAt: serverTimestamp(),
    });

    // 2. Update the product with computed offerPrice
    await updateProductWithOffer(data.productId, originalPrice, data.discount);

    return { success: true, id: docRef.id };
  } catch (error) {
    console.error('Error al crear la oferta:', error);
    return { success: false, error };
  }
};
The private updateProductWithOffer helper computes the price and patches the product:
const updateProductWithOffer = async (
  productId: string,
  originalPrice: number,
  discountPercent: number,
) => {
  const offerPrice = parseFloat(
    (originalPrice * (1 - discountPercent / 100)).toFixed(2)
  );
  await updateDoc(doc(db, 'products', productId), {
    hasOffer: true,
    offerPrice,
    badge: 'Oferta',
    updatedAt: serverTimestamp(),
  });
};

Product Offer Fields

The following fields on the products collection document control offer display throughout the catalog:
FieldTypeDescription
pricenumberRegular selling price in BOB
hasOfferbooleantrue when an active offer is in effect
offerPricenumberDiscounted price, calculated as price × (1 − discount/100) and rounded to 2 decimal places
badgestringLabel shown on the product card — set to "Oferta" when an offer is active
The offerPrice is always derived from the discount percentage applied to the product’s price at the time the offer is saved. If the regular price changes later, the seller must create a new offer to recalculate.

Offer Display in the Catalog — DiscountBadge

The DiscountBadge component in src/features/offers/components/DiscountBadge.tsx renders the buyer-facing price preview. It is also embedded in the OfferForm as a live preview while the seller types a discount value:
// src/features/offers/components/DiscountBadge.tsx
export default function DiscountBadge({ originalPrice, discountPercentage }: DiscountBadgeProps) {
  if (!discountPercentage || discountPercentage <= 0) return null;

  const discountAmount = originalPrice * (discountPercentage / 100);
  const finalPrice     = originalPrice - discountAmount;

  return (
    <div className="flex items-center gap-3 p-3 ...">
      <div className="flex flex-col">
        <span className="text-sm text-text-light/50 line-through">
          Bs. {originalPrice.toFixed(2)}
        </span>
        <span className="text-lg font-bold text-primary">
          Bs. {finalPrice.toFixed(2)}
        </span>
      </div>
      <span className="bg-red-500 text-white text-xs font-bold px-2 py-1 rounded uppercase animate-pulse">
        -{discountPercentage}% Oferta
      </span>
    </div>
  );
}
The original price is struck through with line-through, the discounted price is highlighted in the primary brand color, and the percentage badge pulses in red to attract buyer attention.

OfferForm Validation

The OfferForm component (src/features/offers/components/OfferForm.tsx) enforces the following rules before calling createOfferService:
  1. All fields are required — product, discount percentage, start date, and end date must all be filled.
  2. Discount range — the discount must be a whole number between 1 and 100 (inclusive). Values outside this range produce: “El descuento debe ser un número entre 1 y 100.”
  3. Date order — the end date cannot be earlier than the start date. Violation produces: “La fecha de finalización no puede ser anterior a la de inicio.”
  4. Product must exist — the product must be found in the loaded list. A stale selection triggers: “Producto no encontrado. Recarga la página e intenta de nuevo.”
  5. No duplicate offers — submitting a second offer for the same product while a previous one is active will overwrite hasOffer and offerPrice on the product document with the newer values.
On success a toast notification confirms the offer was saved:
“¡Oferta guardada! ahora aparece con % de descuento en el catálogo.”

Procurement — /seller/purchase

Sellers can register new stock purchases via the /seller/purchase page, powered by PurchaseForm and purchaseService.ts. When a purchase is registered, registerPurchase performs a batched Firestore write that:
  1. Creates a document in the purchases collection with productId, quantity, unitCost, totalCost, purchaseDate, supplier, and sellerId.
  2. Increments stockAvailable and stockTotal on the matching inventory/{productId} document.
  3. Appends an INGRESO_COMPRA movement to inventoryMovements.
// src/features/seller/services/purchaseService.ts
export const registerPurchase = async (data: PurchaseData): Promise<void> => {
  const batch = writeBatch(db);

  // 1. purchases document
  batch.set(purchaseRef, { /* ...purchase fields */ });

  // 2. inventory increment
  batch.update(inventoryRef, {
    stockAvailable: increment(data.quantity),
    stockTotal:     increment(data.quantity),
    updatedAt:      serverTimestamp(),
  });

  // 3. movement log
  batch.set(movementRef, {
    type: 'INGRESO_COMPRA',
    quantity: data.quantity,
    operatorId: data.sellerId,
    reason: `Compra registrada${data.supplier ? ` - Proveedor: ${data.supplier}` : ''}`,
    createdAt: serverTimestamp(),
  });

  await batch.commit();
};
After registering a purchase and confirming the new stock is available, sellers can immediately apply an offer to the restocked product via /seller/offers — the product will appear in the dropdown as soon as active: true is set on the product document.

Build docs developers (and LLMs) love