Skip to main content

Overview

The Product data model represents automotive products and services available for sale in MotorDesk. Products include oils, filters, parts, and services, each with pricing, maintenance intervals, and tax configuration.

TypeScript Interface

interface Product {
  id: string;
  codigoBarras: string;
  nombre: string;
  categoria: string;
  precioVenta: number;
  unidadMedida: string;
  frecuenciaCambioKm: number;
  afectacionIgv: string;
}

Field Reference

id
string
required
Unique identifier for the productFormat: prod-XXX where XXX is a sequential numberExample: prod-001
codigoBarras
string
required
Barcode number for the productFormat: Typically 13-digit EAN/UPC codeExample: 7751234567801Note: Used for quick product lookup at point of sale
nombre
string
required
Product name and descriptionFormat: Free text, typically includes brand, type, and specificationsExamples:
  • Aceite Castrol Magnatec 10W-40 (Galón)
  • Filtro de Aceite Bosh TOY-01
  • Pastillas de Freno Delanteras Toyota
categoria
string
required
Product category for organization and filteringCommon Categories:
  • Aceites de Motor - Motor oils
  • Filtros - Filters (oil, air, fuel)
  • Refrigerantes - Coolants
  • Repuestos - Parts and components
Example: Aceites de Motor
precioVenta
number
required
Sales price per unit (before taxes)Type: Decimal number with 2 decimal placesExample: 120.00, 25.00, 180.00Note: IGV (18%) is calculated separately at sale time
unidadMedida
string
required
Unit of measurement for the productCommon Values (SUNAT codes):
  • NIU - Unit (unidad)
  • GAL - Gallon (galón)
  • LTR - Liter (litro)
  • PAR - Pair (par)
  • KGM - Kilogram
Example: GAL, NIUNote: Must comply with SUNAT’s official unit codes for electronic invoicing
frecuenciaCambioKm
number
required
Recommended maintenance interval in kilometersType: IntegerExample: 5000, 10000, 20000Note: Used to calculate when the next service is due. Set to 0 if not applicable
afectacionIgv
string
required
IGV (tax) applicability codeValid Values (SUNAT codes):
  • 10 - Gravado - Operación Onerosa (Taxable)
  • 20 - Exonerado - Operación Onerosa (Tax exempt)
  • 30 - Inafecto - Operación Onerosa (Not taxed)
Example: 10 (most common - taxable at 18%)Note: Required for SUNAT electronic invoicing

Product Categories

Engine oils with various viscosities and specifications.Common Products:
  • Conventional oils (10W-40, 15W-40)
  • Synthetic oils (5W-40, 0W-20)
  • Semi-synthetic blends
Typical Interval: 5,000 - 10,000 km
Replacement filters for various vehicle systems.Types:
  • Oil filters
  • Air filters
  • Fuel filters
  • Cabin air filters
Typical Interval: 5,000 - 15,000 km
Engine cooling system fluids.Types:
  • Long-life coolant
  • Standard coolant
  • Various colors (green, red, blue)
Typical Interval: 20,000 - 40,000 km
Replacement parts and components.Examples:
  • Brake pads and rotors
  • Spark plugs
  • Belts and hoses
  • Batteries
Typical Interval: Varies by component (15,000 - 50,000 km)

Example Data

Motor Oil Product

{
  "id": "prod-001",
  "codigoBarras": "7751234567801",
  "nombre": "Aceite Castrol Magnatec 10W-40 (Galón)",
  "categoria": "Aceites de Motor",
  "precioVenta": 120.00,
  "unidadMedida": "GAL",
  "frecuenciaCambioKm": 5000,
  "afectacionIgv": "10"
}

Oil Filter Product

{
  "id": "prod-002",
  "codigoBarras": "7751234567802",
  "nombre": "Filtro de Aceite Bosh TOY-01",
  "categoria": "Filtros",
  "precioVenta": 25.00,
  "unidadMedida": "NIU",
  "frecuenciaCambioKm": 5000,
  "afectacionIgv": "10"
}

Brake Pads Product

{
  "id": "prod-005",
  "codigoBarras": "7751234567805",
  "nombre": "Pastillas de Freno Delanteras Toyota",
  "categoria": "Repuestos",
  "precioVenta": 180.00,
  "unidadMedida": "PAR",
  "frecuenciaCambioKm": 30000,
  "afectacionIgv": "10"
}

Relationships

Products are sold through sale detail line items.
// Find all sales containing a specific product
const productSales = saleDetails
  .filter(sd => sd.productId === product.id)
  .map(sd => sales.find(s => s.id === sd.saleId));
Products can be associated with specific vehicles as frequently used items.
// Get usual products for a vehicle
const usualProducts = vehicleUsualProducts
  .filter(vup => vup.vehicleId === vehicleId)
  .map(vup => products.find(p => p.id === vup.productId));

Business Logic

Price Calculation with IGV

When a product is added to a sale, the price needs to be calculated with tax:
const calculateProductPrice = (product: Product, quantity: number) => {
  const subtotal = product.precioVenta * quantity;
  
  // Check if product is taxable (afectacionIgv === "10")
  const igv = product.afectacionIgv === "10" 
    ? subtotal * 0.18 
    : 0;
  
  const total = subtotal + igv;
  
  return {
    cantidad: quantity,
    precioUnitario: product.precioVenta,
    subtotal: parseFloat(subtotal.toFixed(2)),
    igv: parseFloat(igv.toFixed(2)),
    total: parseFloat(total.toFixed(2))
  };
};

Maintenance Interval Tracking

Use product maintenance intervals to determine next service date:
const calculateNextService = (
  currentKm: number,
  soldProducts: Product[]
): number => {
  // Find products with maintenance intervals
  const intervalsKm = soldProducts
    .filter(p => p.frecuenciaCambioKm > 0)
    .map(p => p.frecuenciaCambioKm);
  
  if (intervalsKm.length === 0) {
    return 0; // No products with maintenance intervals
  }
  
  // Use the shortest interval (most frequent service needed)
  const shortestInterval = Math.min(...intervalsKm);
  return currentKm + shortestInterval;
};

Product Search and Filtering

const searchProducts = (searchTerm: string, category?: string) => {
  const term = searchTerm.toLowerCase();
  
  return products.filter(p => {
    // Filter by category if specified
    const categoryMatch = !category || 
                         category === "TODAS" || 
                         p.categoria === category;
    
    // Filter by search term
    const searchMatch = 
      p.nombre.toLowerCase().includes(term) ||
      p.codigoBarras.includes(term) ||
      p.categoria.toLowerCase().includes(term);
    
    return categoryMatch && searchMatch;
  });
};

Barcode Lookup

const findProductByBarcode = (barcode: string): Product | undefined => {
  return products.find(p => p.codigoBarras === barcode);
};

// Usage in POS system
const handleBarcodeScanned = (barcode: string) => {
  const product = findProductByBarcode(barcode);
  
  if (product) {
    // Add to cart
    addToCart(product);
  } else {
    // Show error: product not found
    showError('Producto no encontrado');
  }
};

SUNAT Compliance

Electronic Invoice RequirementsProducts must include:
  • unidadMedida - Official SUNAT unit code
  • afectacionIgv - Tax applicability code
  • Proper pricing with 2 decimal places
These fields are required for generating valid electronic invoices that will be accepted by SUNAT.

Valid SUNAT Unit Codes

const SUNAT_UNITS = {
  NIU: 'Unidad',           // Unit
  GAL: 'Galón',           // Gallon
  LTR: 'Litro',           // Liter
  PAR: 'Par',             // Pair
  KGM: 'Kilogramo',       // Kilogram
  MTR: 'Metro',           // Meter
  SET: 'Juego',           // Set
  DZN: 'Docena',          // Dozen
  BX: 'Caja',             // Box
};

Validation Rules

Required Validations
  • codigoBarras must be unique across all products
  • precioVenta must be greater than 0
  • frecuenciaCambioKm must be 0 or greater
  • unidadMedida must be a valid SUNAT unit code
  • afectacionIgv must be a valid SUNAT tax code

Data Storage

Mock Data Location

/src/data/mock/products.tsTypeScript export with sample product data

JSON Data Location

/src/data/json/products.jsonRaw JSON array of product objects
  • Sales - Products are sold through sales transactions
  • Sale Details - Line items linking products to sales
  • Vehicles - Products have maintenance intervals for vehicles

Usage Examples

Get All Categories

const getProductCategories = (): string[] => {
  const categories = new Set(products.map(p => p.categoria));
  return Array.from(categories).sort();
};

Products by Category

const getProductsByCategory = (category: string) => {
  return products
    .filter(p => p.categoria === category)
    .sort((a, b) => a.nombre.localeCompare(b.nombre));
};

Calculate Cart Total

interface CartItem {
  product: Product;
  cantidad: number;
}

const calculateCartTotal = (cartItems: CartItem[]) => {
  let subtotal = 0;
  let igv = 0;
  
  cartItems.forEach(item => {
    const itemSubtotal = item.product.precioVenta * item.cantidad;
    subtotal += itemSubtotal;
    
    // Add IGV if product is taxable
    if (item.product.afectacionIgv === "10") {
      igv += itemSubtotal * 0.18;
    }
  });
  
  return {
    subtotal: parseFloat(subtotal.toFixed(2)),
    igv: parseFloat(igv.toFixed(2)),
    total: parseFloat((subtotal + igv).toFixed(2)),
    itemCount: cartItems.length,
    totalUnits: cartItems.reduce((sum, item) => sum + item.cantidad, 0)
  };
};

Product Sales Report

const getProductSalesReport = (productId: string, dateRange?: { start: string, end: string }) => {
  // Get all sale details for this product
  let productSaleDetails = saleDetails.filter(sd => sd.productId === productId);
  
  // Filter by date if provided
  if (dateRange) {
    const salesInRange = sales.filter(s => {
      const saleDate = new Date(s.fechaEmision).getTime();
      return saleDate >= new Date(dateRange.start).getTime() &&
             saleDate <= new Date(dateRange.end).getTime();
    });
    
    productSaleDetails = productSaleDetails.filter(sd =>
      salesInRange.some(s => s.id === sd.saleId)
    );
  }
  
  // Calculate statistics
  const totalQuantity = productSaleDetails.reduce(
    (sum, sd) => sum + sd.cantidad, 0
  );
  
  const totalRevenue = productSaleDetails.reduce(
    (sum, sd) => sum + sd.subtotal, 0
  );
  
  return {
    productId,
    product: products.find(p => p.id === productId),
    totalSales: productSaleDetails.length,
    totalQuantity,
    totalRevenue: parseFloat(totalRevenue.toFixed(2)),
    averageQuantityPerSale: totalQuantity / productSaleDetails.length || 0
  };
};

Build docs developers (and LLMs) love