ifixkart-storefront/utils/productMapper.ts

182 lines
6.3 KiB
TypeScript

import { ProductResponse, ProductCardResponse } from '../services/api/catalogService';
import { Product } from '../types';
import { getCatalogPriceInfo, parseMoney } from '../lib/utils';
/**
* Normalize a list/card API product into a full ProductResponse with numeric variant prices.
*/
export function mapProductCardToProductResponse(
p: ProductCardResponse | Record<string, any>
): ProductResponse {
const price = parseMoney(p.price);
const comparePrice = p.compare_price != null ? parseMoney(p.compare_price) : null;
const colorsFromCard = Array.isArray((p as any).colors)
? (p as any).colors.filter(Boolean)
: (p as any).color
? [(p as any).color]
: [];
const variants =
Array.isArray((p as any).variants) && (p as any).variants.length > 0
? (p as any).variants.map((v: any) => ({
...v,
price: parseMoney(v.price),
compare_price:
v.compare_price != null && v.compare_price !== ''
? parseMoney(v.compare_price)
: null,
cost_price: parseMoney(v.cost_price),
attributes: Array.isArray(v.attributes) ? v.attributes : [],
}))
: [
{
variant_id: p.first_variant_id || p.product_id,
product_id: p.product_id,
sku: (p as any).sku || '',
barcode: null,
price,
compare_price: comparePrice && comparePrice > 0 ? comparePrice : null,
cost_price: 0,
low_stock_threshold: 5,
status: 'active',
available_stock: p.stock_count,
attributes: colorsFromCard.map((color: string, index: number) => ({
id: `color-${index}`,
variant_id: p.first_variant_id || p.product_id,
attribute_id: `color-${index}`,
attribute_value: color,
attribute_name: 'Color',
attribute_code: 'color',
})),
images: [],
},
];
return {
product_id: p.product_id,
category_id: p.category_id || '',
brand_id: (p as any).brand_id || null,
device_series_id: (p as any).device_series_id || null,
device_model_id: (p as any).device_model_id || null,
name: p.name,
slug: p.slug,
full_path: (p as any).full_path || `/products/${p.slug}`,
description: (p as any).description ?? null,
status: (p as any).status || 'active',
created_at: (p as any).created_at || '',
rating: (() => {
const n = Number((p as any).rating ?? (p as any).average_rating ?? (p as any).avg_rating);
return Number.isFinite(n) && n > 0 ? n : undefined;
})(),
badge: (p.badge as ProductResponse['badge']) || null,
review_count: (p as any).review_count || 12,
discount_percent: parseMoney((p as any).discount_percent) || undefined,
color: (p as any).color || colorsFromCard[0] || null,
colors: colorsFromCard,
brand_name: (p as any).brand_name || null,
images:
(p as any).images?.length > 0
? (p as any).images
: p.thumbnail_url
? [
{
image_id: '1',
product_id: p.product_id,
image_url: p.thumbnail_url,
alt_text: p.name,
sort_order: 0,
is_banner: true,
},
]
: [],
variants,
} as ProductResponse;
}
/**
* Standardizes the mapping from database ProductResponse / ProductCardResponse (from API)
* to the storefront's local Product interface.
*/
export function mapCatalogProductToProduct(
p: ProductResponse | ProductCardResponse | Record<string, any>
): Product {
const normalized: ProductResponse =
Array.isArray((p as any).variants) && (p as any).variants.length > 0
? ({
...mapProductCardToProductResponse(p),
...p,
variants: (p as any).variants.map((v: any) => ({
...v,
price: parseMoney(v.price),
compare_price:
v.compare_price != null && v.compare_price !== ''
? parseMoney(v.compare_price)
: null,
})),
} as ProductResponse)
: mapProductCardToProductResponse(p);
const { price, priceMax, oldPrice, discount } = getCatalogPriceInfo(normalized);
const hasTimer = false;
return {
id: normalized.slug,
product_id: normalized.product_id,
first_variant_id:
(p as any).first_variant_id ||
normalized.variants?.[0]?.variant_id ||
undefined,
slug: normalized.slug,
name: normalized.name,
price,
priceMax: priceMax && priceMax > price ? priceMax : undefined,
oldPrice: oldPrice && oldPrice > price ? oldPrice : undefined,
rating: normalized.rating || 5.0,
reviewsCount: normalized.review_count || 12,
image:
(p as any).thumbnail_url ||
(normalized.images && normalized.images[0] ? normalized.images[0].image_url : ''),
imageHover:
normalized.images && normalized.images[1]
? normalized.images[1].image_url
: (p as any).thumbnail_url || undefined,
discount,
category: normalized.category_id,
brand: normalized.brand_id || (p as any).brand_name || 'iFixKart',
// Deal countdown belongs only on the Deals Of The Day card — not catalog tiles
hasTimer,
timeRemaining: undefined,
isHot: normalized.badge === 'HOT',
isFeatured: normalized.badge === 'HOT' || (normalized.rating !== undefined && normalized.rating >= 4.8),
isNewArrival: normalized.badge === 'NEW' || !normalized.badge,
isRecentlyLaunched: normalized.badge === 'NEW',
isTechGadget: true,
totalStock: typeof (p as any).stock_count === 'number' ? (p as any).stock_count : 50,
soldCount: 15,
};
}
/**
* Deterministically selects the Hot Deal product from a list.
* Priority:
* 1. Product tagged "Hot Deal" (isHot === true)
* 2. Product with highest discount percentage
* 3. Fallback to first available product
*/
export function getHotDealProduct(products: Product[]): Product | null {
if (!products || products.length === 0) return null;
const hotTagged = products.find((p) => p.isHot);
if (hotTagged) return hotTagged;
const sortedByDiscount = [...products].sort(
(a, b) => (b.discount || 0) - (a.discount || 0)
);
if (sortedByDiscount[0] && (sortedByDiscount[0].discount || 0) > 0) {
return sortedByDiscount[0];
}
return products[0];
}