Files
temp_SSA_SCAN/lib/product-api.ts

358 lines
13 KiB
TypeScript

export interface ProductImage {
imageId: string;
imageLabel: string;
imageTag: string;
imageUrl: string;
imageText: string;
imageLastModified: string;
}
export interface CommercialOffer {
Price: number;
ListPrice: number;
PriceWithoutDiscount: number;
FullSellingPrice: number;
RewardValue: number;
PriceValidUntil: string;
AvailableQuantity: number;
IsAvailable: boolean;
GiftSkuIds: string[];
Installments: {
Value: number;
InterestRate: number;
TotalValuePlusInterestRate: number;
NumberOfInstallments: number;
}[];
discountHighLight: unknown[];
teasers: unknown[];
}
export interface ProductSeller {
sellerId: string;
sellerName: string;
addToCartLink: string;
sellerDefault: boolean;
commertialOffer: CommercialOffer;
}
export interface ProductItem {
itemId: string;
name: string;
nameComplete: string;
complementName: string;
ean: string;
referenceId: { Key: string; Value: string }[];
measurementUnit: string;
unitMultiplier: number;
modalType: null;
isKit: boolean;
images: ProductImage[];
Talla: string[];
variations: string[];
sellers: ProductSeller[];
Videos: unknown[];
estimatedDateArrival: null;
Tipo?: string[];
Ocasión?: string[];
Género?: string[];
Acabado?: string[];
Color?: string[];
Estilo?: string[];
Disciplina?: string[];
}
export interface Product {
productId: string;
productName: string;
brand: string;
brandId: number;
brandImageUrl: null | string;
linkText: string;
productReference: string;
productReferenceCode: string;
categoryId: string;
productTitle: string;
metaTagDescription: string;
releaseDate: string;
clusterHighlights: Record<string, unknown>;
productClusters: Record<string, string>;
searchableClusters: Record<string, unknown>;
categories: string[];
categoriesIds: string[];
link: string;
description: string;
items: ProductItem[];
// Additional fields that appear at product level
Color?: string[];
Género?: string[];
Ocasión?: string[];
Tipo?: string[];
Acabado?: string[];
Estilo?: string[];
Disciplina?: string[];
}
// Fallback product ID used only when no productId is provided
// This should rarely happen - normally productId comes from SKU detection or history
const FIXED_PRODUCT_ID = '180474';
export async function fetchProduct(productId: string = FIXED_PRODUCT_ID): Promise<Product | null> {
try {
// Use our Next.js API route as a proxy to avoid CORS issues
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error('Failed to fetch product:', response.status, errorData);
return null;
}
const product = await response.json();
return product;
} catch (error) {
console.error('Error fetching product:', error);
return null;
}
}
export function getProductImages(product: Product): string[] {
const allImages: string[] = [];
product.items.forEach(item => {
item.images.forEach(image => {
if (image.imageUrl && !allImages.includes(image.imageUrl)) {
allImages.push(image.imageUrl);
}
});
});
return allImages;
}
export function getProductPricing(product: Product) {
const firstAvailableItem = product.items.find(item =>
item.sellers.some(seller => seller.commertialOffer.IsAvailable)
);
if (!firstAvailableItem) {
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
}
const seller = firstAvailableItem.sellers.find(s => s.commertialOffer.IsAvailable);
if (!seller) {
return { price: 0, listPrice: 0, discount: 0, isAvailable: false };
}
const { Price, ListPrice } = seller.commertialOffer;
const discount = ListPrice > Price ? Math.round(((ListPrice - Price) / ListPrice) * 100) : 0;
return {
price: Price,
listPrice: ListPrice,
discount,
isAvailable: true
};
}
export function getProductVariants(product: Product) {
return product.items.map(item => {
const availableSeller = item.sellers.find(s => s.commertialOffer.IsAvailable);
return {
itemId: item.itemId,
name: item.name,
sizes: item.Talla,
images: item.images.map(img => img.imageUrl),
isAvailable: item.sellers.some(seller => seller.commertialOffer.IsAvailable),
price: availableSeller?.commertialOffer.Price || 0,
availableQuantity: availableSeller?.commertialOffer.AvailableQuantity || 0,
seller: availableSeller || item.sellers[0] // Fallback to first seller
};
});
}
export function getProductCategories(product: Product) {
return product.categories.map(category => {
// Remove leading/trailing slashes and split by '/'
const parts = category.replace(/^\/|\/$/g, '').split('/');
return parts.filter(part => part.length > 0);
}).flat();
}
export function getProductClusters(product: Product) {
return Object.entries(product.productClusters).map(([id, name]) => ({
id,
name,
displayName: name.charAt(0).toUpperCase() + name.slice(1).toLowerCase()
}));
}
export function getStockStatus(availableQuantity: number) {
if (availableQuantity === 0) return { status: 'out_of_stock', label: 'Agotado', color: 'text-red-400' };
if (availableQuantity === 1) return { status: 'low_stock', label: 'Última pieza', color: 'text-yellow-400' };
if (availableQuantity <= 3) return { status: 'limited', label: `Solo ${availableQuantity}`, color: 'text-orange-400' };
return { status: 'in_stock', label: 'Disponible', color: 'text-green-400' };
}
export function getProductGender(product: Product) {
// Get gender from product level first, then fallback to items
const gender = product.Género?.[0] || product.items[0]?.Género?.[0];
if (!gender) return null;
// Map gender to display info
const genderMap: Record<string, { label: string; icon: string; color: string }> = {
'Hombre': { label: 'Hombre', icon: '♂', color: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
'Mujer': { label: 'Mujer', icon: '♀', color: 'bg-pink-500/20 text-pink-300 border-pink-500/30' },
'Niño': { label: 'Niño', icon: '👶', color: 'bg-green-500/20 text-green-300 border-green-500/30' },
'Unisex': { label: 'Unisex', icon: '⚧', color: 'bg-purple-500/20 text-purple-300 border-purple-500/30' }
};
return genderMap[gender] || { label: gender, icon: '👤', color: 'bg-gray-500/20 text-gray-300 border-gray-500/30' };
}
export function getProductSeason(product: Product) {
// Check product clusters for season info
const seasonCluster = Object.values(product.productClusters).find(cluster => {
const clusterLower = cluster.toLowerCase();
return clusterLower.includes('oi') || clusterLower.includes('pv') ||
clusterLower.includes('fw') || clusterLower.includes('ss') ||
clusterLower.includes('2024') || clusterLower.includes('2025');
});
if (!seasonCluster) return null;
// Map season patterns
if (seasonCluster.toLowerCase().includes('oi') || seasonCluster.toLowerCase().includes('fw')) {
return { label: 'Otoño-Invierno', season: seasonCluster, color: 'bg-orange-500/20 text-orange-300 border-orange-500/30', icon: '🍂' };
}
if (seasonCluster.toLowerCase().includes('pv') || seasonCluster.toLowerCase().includes('ss')) {
return { label: 'Primavera-Verano', season: seasonCluster, color: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30', icon: '🌸' };
}
// Return generic season info
return { label: 'Temporada', season: seasonCluster, color: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30', icon: '📅' };
}
export function getProductOccasion(product: Product) {
// Get occasion from product level first, then fallback to items
const occasion = product.Ocasión?.[0] || product.items[0]?.Ocasión?.[0];
if (!occasion) return null;
const occasionMap: Record<string, { label: string; color: string; icon: string }> = {
'Casual': { label: 'Casual', color: 'bg-teal-500/20 text-teal-300 border-teal-500/30', icon: '👟' },
'Deportivo': { label: 'Deportivo', color: 'bg-red-500/20 text-red-300 border-red-500/30', icon: '🏃' },
'Formal': { label: 'Formal', color: 'bg-slate-500/20 text-slate-300 border-slate-500/30', icon: '👔' },
'Trabajo': { label: 'Trabajo', color: 'bg-amber-500/20 text-amber-300 border-amber-500/30', icon: '💼' }
};
return occasionMap[occasion] || { label: occasion, color: 'bg-gray-500/20 text-gray-300 border-gray-500/30', icon: '🏷️' };
}
export function getProductColors(product: Product) {
// Get colors from product level first (main approach for this API)
const allColors = new Set<string>();
// Check product-level color field (this is where the color is stored in this API)
if (product.Color && Array.isArray(product.Color)) {
product.Color.forEach(color => allColors.add(color));
}
// Fallback: check items level
product.items.forEach(item => {
if (item.Color && Array.isArray(item.Color)) {
item.Color.forEach(color => allColors.add(color));
}
});
const colors = Array.from(allColors);
if (colors.length === 0) return [];
// Map colors to display info with hex approximations
return colors.map(color => {
const colorLower = color.toLowerCase();
const colorMap: Record<string, { label: string; hex: string; badge: string }> = {
'negro': { label: 'Negro', hex: '#1f2937', badge: 'bg-gray-800 text-white border-gray-700' },
'blanco': { label: 'Blanco', hex: '#f9fafb', badge: 'bg-gray-100 text-gray-900 border-gray-300' },
'rojo': { label: 'Rojo', hex: '#dc2626', badge: 'bg-red-500/20 text-red-300 border-red-500/30' },
'azul': { label: 'Azul', hex: '#2563eb', badge: 'bg-blue-500/20 text-blue-300 border-blue-500/30' },
'verde': { label: 'Verde', hex: '#16a34a', badge: 'bg-green-500/20 text-green-300 border-green-500/30' },
'amarillo': { label: 'Amarillo', hex: '#ca8a04', badge: 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' },
'rosa': { label: 'Rosa', hex: '#ec4899', badge: 'bg-pink-500/20 text-pink-300 border-pink-500/30' },
'morado': { label: 'Morado', hex: '#9333ea', badge: 'bg-purple-500/20 text-purple-300 border-purple-500/30' },
'naranja': { label: 'Naranja', hex: '#ea580c', badge: 'bg-orange-500/20 text-orange-300 border-orange-500/30' },
'gris': { label: 'Gris', hex: '#6b7280', badge: 'bg-gray-500/20 text-gray-300 border-gray-500/30' },
'marrón': { label: 'Marrón', hex: '#92400e', badge: 'bg-amber-700/20 text-amber-300 border-amber-700/30' },
'café': { label: 'Café', hex: '#92400e', badge: 'bg-amber-700/20 text-amber-300 border-amber-700/30' },
'beige': { label: 'Beige', hex: '#d6d3d1', badge: 'bg-stone-300/20 text-stone-300 border-stone-300/30' },
'dorado': { label: 'Dorado', hex: '#f59e0b', badge: 'bg-yellow-600/20 text-yellow-300 border-yellow-600/30' },
'plateado': { label: 'Plateado', hex: '#9ca3af', badge: 'bg-gray-400/20 text-gray-300 border-gray-400/30' }
};
// Try exact match first
const exactMatch = colorMap[colorLower];
if (exactMatch) {
return { ...exactMatch, original: color };
}
// Try partial matches for compound colors
const partialMatch = Object.entries(colorMap).find(([key]) =>
colorLower.includes(key) || key.includes(colorLower)
);
if (partialMatch) {
return { ...partialMatch[1], original: color };
}
// Default fallback
return {
label: color,
hex: '#64748b',
badge: 'bg-slate-500/20 text-slate-300 border-slate-500/30',
original: color
};
});
}
export function getProductHighlight(product: Product) {
// Only show badge if clusterHighlights has actual content
if (!product.clusterHighlights || Object.keys(product.clusterHighlights).length === 0) {
return null;
}
// Show the highlight from clusterHighlights
const highlight = Object.values(product.clusterHighlights)[0];
return {
label: String(highlight),
style: 'bg-gradient-to-br from-yellow-500 to-orange-600 text-white shadow-lg shadow-yellow-500/50',
icon: '⭐',
type: 'promotion'
};
}
/**
* Loads test product data from JSON file
* Used when SKU detection returns "test_product"
*
* The test product data is stored in /public/test-product.json
* and can be edited manually without changing code
*/
export async function fetchTestProduct(): Promise<Product | null> {
try {
const response = await fetch('/test-product.json');
if (!response.ok) {
console.error('Failed to load test product JSON:', response.status);
return null;
}
const testProduct: Product = await response.json();
console.log('✅ Test product loaded from JSON successfully');
return testProduct;
} catch (error) {
console.error('Error loading test product:', error);
return null;
}
}