diff --git a/apps/backend/src/lib/post-order-handler.ts b/apps/backend/src/lib/post-order-handler.ts index 29dd536..13abcd0 100644 --- a/apps/backend/src/lib/post-order-handler.ts +++ b/apps/backend/src/lib/post-order-handler.ts @@ -48,7 +48,9 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => { message += 'šŸ“¦ Items:\n'; order.orderItems?.forEach((item: any) => { - message += ` • ${item.product?.name || 'Unknown'} • ${item.product.productQuantity}${item.product.unit?.shortNotation}x${item.quantity}\n`; + const sku = item.sku + const features = (sku?.features || []).map((f: any) => f.featureValue).join(' ') + message += ` • ${sku?.product?.name || 'Unknown'} ${features} x${item.quantity}\n`; }); message += `\nšŸ’° Total: ₹${order.totalAmount}\n`; @@ -87,7 +89,7 @@ const formatCancellationMessage = (orderData: any, cancellationData: Cancellatio šŸ“ž Phone: ${orderData.address?.phone || 'N/A'} šŸ“¦ Items: -${orderData.orderItems?.map((item: any) => ` • ${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'} +${orderData.orderItems?.map((item: any) => ` • ${item.sku?.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'} šŸ’° Total: ₹${orderData.totalAmount} šŸ’³ Refund: ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'} diff --git a/apps/backend/src/sqliteImporter.ts b/apps/backend/src/sqliteImporter.ts index c245555..d685475 100644 --- a/apps/backend/src/sqliteImporter.ts +++ b/apps/backend/src/sqliteImporter.ts @@ -282,7 +282,7 @@ export { type OrderWithFullData, type OrderWithCancellationData, // Common API helpers - getSuspendedProductIds, + getSuspendedSkuIds, getNextDeliveryDateWithCapacity, getStoresSummary, healthCheck, diff --git a/apps/backend/src/stores/product-store.ts b/apps/backend/src/stores/product-store.ts index 4c981b5..2b500bf 100644 --- a/apps/backend/src/stores/product-store.ts +++ b/apps/backend/src/stores/product-store.ts @@ -76,18 +76,18 @@ export async function initializeProducts(): Promise { const allDeliverySlots = await getAllDeliverySlotsForCache() const deliverySlotsMap = new Map() for (const slot of allDeliverySlots) { - if (!deliverySlotsMap.has(slot.productId)) - deliverySlotsMap.set(slot.productId, []) - deliverySlotsMap.get(slot.productId)!.push(slot) + if (!deliverySlotsMap.has(slot.skuId)) + deliverySlotsMap.set(slot.skuId, []) + deliverySlotsMap.get(slot.skuId)!.push(slot) } // Fetch all special deals const allSpecialDeals = await getAllSpecialDealsForCache() const specialDealsMap = new Map() for (const deal of allSpecialDeals) { - if (!specialDealsMap.has(deal.productId)) - specialDealsMap.set(deal.productId, []) - specialDealsMap.get(deal.productId)!.push(deal) + if (!specialDealsMap.has(deal.skuId)) + specialDealsMap.set(deal.skuId, []) + specialDealsMap.get(deal.skuId)!.push(deal) } // Fetch all product tags @@ -153,70 +153,11 @@ export async function initializeProducts(): Promise { export async function getProductById(id: number): Promise { try { - // const key = `product:${id}` - // const data = await redisClient.get(key) - // if (!data) return null - // return JSON.parse(data) as Product - - const product = await getProductByIdFromDb(id) - if (!product) return null - - const signedImages = scaffoldAssetUrl( - (product.images as string[]) || [] - ) - - // Fetch store info - const allStores = await getAllStoresForCache() - const store = product.storeId - ? allStores.find(s => s.id === product.storeId) || null - : null - - // Fetch delivery slots for this product - const allDeliverySlots = await getAllDeliverySlotsForCache() - const productSlots = allDeliverySlots.filter(s => s.productId === id) - - // Fetch special deals for this product - const allSpecialDeals = await getAllSpecialDealsForCache() - const productDeals = allSpecialDeals.filter(d => d.productId === id) - - // Fetch product tags for this product - const allProductTags = await getAllProductTagsForCache() - const productTagNames = allProductTags - .filter(t => t.productId === id) - .map(t => t.tagName) - - return { - id: product.id, - name: product.name, - shortDescription: product.shortDescription, - longDescription: product.longDescription, - price: product.price.toString(), - marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unit.shortNotation, - images: signedImages, - isOutOfStock: product.isOutOfStock, - store: store - ? { id: store.id, name: store.name, description: store.description } - : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: productSlots.map((s) => ({ - id: s.id, - deliveryTime: s.deliveryTime, - freezeTime: s.freezeTime, - isCapacityFull: s.isCapacityFull, - })), - specialDeals: productDeals.map((d) => ({ - quantity: d.quantity.toString(), - price: d.price.toString(), - validTill: d.validTill, - })), - productTags: productTagNames, - } + const allProducts = await getAllProducts() + const product = allProducts.find(p => p.id === id) + return product || null } catch (error) { - console.error(`Error getting product ${id}:`, error) + console.error('Error getting product by ID:', error) return null } } @@ -250,17 +191,17 @@ export async function getAllProducts(): Promise { const allDeliverySlots = await getAllDeliverySlotsForCache() const deliverySlotsMap = new Map() for (const slot of allDeliverySlots) { - if (!deliverySlotsMap.has(slot.productId)) - deliverySlotsMap.set(slot.productId, []) - deliverySlotsMap.get(slot.productId)!.push(slot) + if (!deliverySlotsMap.has(slot.skuId)) + deliverySlotsMap.set(slot.skuId, []) + deliverySlotsMap.get(slot.skuId)!.push(slot) } const allSpecialDeals = await getAllSpecialDealsForCache() const specialDealsMap = new Map() for (const deal of allSpecialDeals) { - if (!specialDealsMap.has(deal.productId)) - specialDealsMap.set(deal.productId, []) - specialDealsMap.get(deal.productId)!.push(deal) + if (!specialDealsMap.has(deal.skuId)) + specialDealsMap.set(deal.skuId, []) + specialDealsMap.get(deal.skuId)!.push(deal) } const allProductTags = await getAllProductTagsForCache() @@ -281,7 +222,7 @@ export async function getAllProducts(): Promise { : null const deliverySlots = deliverySlotsMap.get(product.id) || [] const specialDeals = specialDealsMap.get(product.id) || [] - const productTags = productTagsMap.get(product.id) || [] + const productTags = productTagsMap.get(product.productId) || [] products.push({ id: product.id, @@ -290,7 +231,7 @@ export async function getAllProducts(): Promise { longDescription: product.longDescription, price: product.price.toString(), marketPrice: product.marketPrice?.toString() || null, - unitNotation: product.unitShortNotation, + unitNotation: product.unitNotation, images: signedImages, isOutOfStock: product.isOutOfStock, store: store diff --git a/apps/backend/src/trpc/apis/common-apis/common.ts b/apps/backend/src/trpc/apis/common-apis/common.ts index 2869025..b9522fc 100644 --- a/apps/backend/src/trpc/apis/common-apis/common.ts +++ b/apps/backend/src/trpc/apis/common-apis/common.ts @@ -1,6 +1,6 @@ import { router, publicProcedure } from '@/src/trpc/trpc-index' import { - getSuspendedProductIds, + getSuspendedSkuIds, getNextDeliveryDateWithCapacity, getStoresSummary, getAllSkusSummary as getAllSkusSummaryInDb, @@ -30,10 +30,10 @@ export async function scaffoldProducts() { .where(eq(productInfo.isSuspended, true)); */ - const suspendedProductIds = new Set(await getSuspendedProductIds()); + const suspendedSkuIds = new Set(await getSuspendedSkuIds()); // Filter out suspended products - products = products.filter(product => !suspendedProductIds.has(product.id)); + products = products.filter(product => !suspendedSkuIds.has(product.id)); // Format products to match the expected response structure const formattedProducts = await Promise.all( diff --git a/apps/backend/src/trpc/apis/user-apis/apis/order.ts b/apps/backend/src/trpc/apis/user-apis/apis/order.ts index 1df4cd4..32548fc 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/order.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/order.ts @@ -46,7 +46,7 @@ import type { const placeOrderUtil = async (params: { userId: number; selectedItems: Array<{ - productId: number; + skuId: number; quantity: number; slotId: number | null; }>; @@ -86,7 +86,7 @@ const placeOrderUtil = async (params: { const ordersBySlot = new Map< number | null, Array<{ - productId: number; + skuId: number; quantity: number; slotId: number | null; product: Awaited>; @@ -94,9 +94,9 @@ const placeOrderUtil = async (params: { >(); for (const item of selectedItems) { - const product = await getOrderProductById(item.productId); + const product = await getOrderProductById(item.skuId); if (!product) { - throw new ApiError(`Product ${item.productId} not found`, 400); + throw new ApiError(`Product ${item.skuId} not found`, 400); } if (!ordersBySlot.has(item.slotId)) { @@ -107,9 +107,9 @@ const placeOrderUtil = async (params: { if (params.isFlash) { for (const item of selectedItems) { - const product = await getOrderProductById(item.productId); + const product = await getOrderProductById(item.skuId); if (!product?.isFlashAvailable) { - throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400); + throw new ApiError(`Product ${item.skuId} is not available for flash delivery`, 400); } } } @@ -198,7 +198,7 @@ const placeOrderUtil = async (params: { return { orderId: 0, - productId: item.productId, + skuId: item.skuId, quantity: item.quantity.toString(), price: priceString, discountedPrice: priceString, @@ -225,7 +225,7 @@ const placeOrderUtil = async (params: { await deleteUserCartItemsForOrder( userId, - selectedItems.map((item) => item.productId) + selectedItems.map((item) => item.skuId) ); if (appliedCoupon && createdOrders.length > 0) { @@ -252,7 +252,7 @@ export const orderRouter = router({ z.object({ selectedItems: z.array( z.object({ - productId: z.number().int().positive(), + skuId: z.number().int().positive(), quantity: z.number().int().positive(), slotId: z.union([z.number().int(), z.null()]), }) @@ -373,13 +373,14 @@ export const orderRouter = router({ const items = await Promise.all( order.orderItems.map(async (item) => { - const signedImages = item.product.images + const signedImages = item.sku?.images ? scaffoldAssetUrl( - item.product.images as string[] + item.sku.images as string[] ) : []; return { - productName: item.product.name, + productName: item.sku?.product?.name || 'Unknown', + skuName: item.sku?.name || null, quantity: parseFloat(item.quantity), price: parseFloat(item.price.toString()), discountedPrice: parseFloat( @@ -512,13 +513,14 @@ export const orderRouter = router({ const items = await Promise.all( order.orderItems.map(async (item) => { - const signedImages = item.product.images + const signedImages = item.sku?.images ? scaffoldAssetUrl( - item.product.images as string[] + item.sku.images as string[] ) : []; return { - productName: item.product.name, + productName: item.sku?.product?.name || 'Unknown', + skuName: item.sku?.name || null, quantity: parseFloat(item.quantity), price: parseFloat(item.price.toString()), discountedPrice: parseFloat( diff --git a/apps/user-ui/app/(drawer)/(tabs)/flash-delivery/(products)/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/flash-delivery/(products)/index.tsx index 8143020..8bd2651 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/flash-delivery/(products)/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/flash-delivery/(products)/index.tsx @@ -26,7 +26,7 @@ const FlashAddToCartDialog = () => { React.useEffect(() => { if (isOpen && product) { addToCart.mutate( - { productId: product.id, quantity: 1, slotId: 0 }, + { skuId: product.id, quantity: 1, slotId: 0 }, { onSuccess: () => { clearAddedFlashProduct(); diff --git a/apps/user-ui/components/BannerCarousel.tsx b/apps/user-ui/components/BannerCarousel.tsx index 893f8b0..6d0a811 100644 --- a/apps/user-ui/components/BannerCarousel.tsx +++ b/apps/user-ui/components/BannerCarousel.tsx @@ -12,7 +12,7 @@ interface Banner { name: string; imageUrl: string; description?: string | null; - productIds?: number[] | null; + skuIds?: number[] | null; redirectUrl?: string | null; serialNum?: number | null; isActive: boolean; @@ -61,13 +61,13 @@ export default function BannerCarousel() { if (error || !banners || banners.length === 0) return null; const handleBannerPress = (banner: Banner) => { - if (banner.productIds && banner.productIds.length > 0) { + if (banner.skuIds && banner.skuIds.length > 0) { // Navigate to the first product's detail page - router.push(`/(drawer)/(tabs)/home/product-detail/${banner.productIds[0]}`); + router.push(`/(drawer)/(tabs)/home/product-detail/${banner.skuIds[0]}`); } else if (banner.redirectUrl) { // Handle external URL - could open in browser or handle deep links } - // If no productIds or redirectUrl, banner is just for display + // If no skuIds or redirectUrl, banner is just for display }; const handleScroll = (event: NativeSyntheticEvent) => { diff --git a/apps/user-ui/components/PaymentAndOrderComponent.tsx b/apps/user-ui/components/PaymentAndOrderComponent.tsx index 12578f7..a088e46 100644 --- a/apps/user-ui/components/PaymentAndOrderComponent.tsx +++ b/apps/user-ui/components/PaymentAndOrderComponent.tsx @@ -128,10 +128,10 @@ const PaymentAndOrderComponent: React.FC = ({ const availableItems = cartItems .filter(item => { - if (productSlotsMap[item.productId]?.isOutOfStock) return false; + if (productSlotsMap[item.skuId]?.isOutOfStock) return false; // For flash delivery, check if product supports flash delivery if (isFlashDelivery) { - return flashEligibleProductIds.has(item.productId); + return flashEligibleProductIds.has(item.skuId); } // For regular delivery, only include items with assigned slots return selectedSlots[item.id]; @@ -150,7 +150,7 @@ const PaymentAndOrderComponent: React.FC = ({ selectedItems: availableItems.map(itemId => { const item = cartItems.find(cartItem => cartItem.id === itemId); return { - productId: item.productId, + skuId: item.skuId, quantity: item.quantity, slotId: isFlashDelivery ? null : selectedSlots[itemId] }; diff --git a/apps/user-ui/components/ProductCard.tsx b/apps/user-ui/components/ProductCard.tsx index 8ee25d1..6d3e401 100644 --- a/apps/user-ui/components/ProductCard.tsx +++ b/apps/user-ui/components/ProductCard.tsx @@ -78,7 +78,7 @@ const ProductCard: React.FC = ({ }) || {}; // Find current quantity from cart data - const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); + const cartItem = cartData?.items?.find((cartItem: any) => cartItem.skuId === item.id); const quantity = cartItem?.quantity || 0; // Get slots data from central store diff --git a/apps/user-ui/components/ProductDetail.tsx b/apps/user-ui/components/ProductDetail.tsx index d4ce7a5..02205ca 100644 --- a/apps/user-ui/components/ProductDetail.tsx +++ b/apps/user-ui/components/ProductDetail.tsx @@ -83,7 +83,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver }, [slotsData, productDetail]) // Find current quantity from cart data - const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null; + const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.skuId === productDetail.id) : null; const quantity = cartItem?.quantity || 0; const handleQuantityChange = (newQuantity: number) => { @@ -143,7 +143,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver }; const handleSlotAddToCart = (productId: number, selectedSlotId: number) => { - const cartItem = cartData.data?.items?.find((item: any) => item.productId === productId); + const cartItem = cartData.data?.items?.find((item: any) => item.skuId === productId); setIsLoadingDialogOpen(true); if (cartItem) { removeFromCart.mutate( diff --git a/apps/user-ui/components/SlotSpecificView.tsx b/apps/user-ui/components/SlotSpecificView.tsx index 7798640..f4977e8 100644 --- a/apps/user-ui/components/SlotSpecificView.tsx +++ b/apps/user-ui/components/SlotSpecificView.tsx @@ -252,7 +252,7 @@ const CompactProductCard = ({ refetchCart: true, }, cartType); - const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id); + const cartItem = cartData?.items?.find((cartItem: any) => cartItem.skuId === item.id); const quantity = cartItem?.quantity || 0; const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock; diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx index 0bb4723..1b4b897 100644 --- a/apps/user-ui/components/cart-page.tsx +++ b/apps/user-ui/components/cart-page.tsx @@ -49,7 +49,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { } = useGetCart({ refetchOnWindowFocus: true }, cartType); // Extract product IDs from cart items - const productIds = cartData?.items.map(item => item.productId) || []; + const productIds = cartData?.items.map(item => item.skuId) || []; // Get cart slots for the products in cart const { data: slotsData, refetch: refetchSlots, isLoading: isSlotsLoading } = trpc.user.cart.getCartSlots.useQuery( @@ -106,9 +106,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { const baseTotalPrice = useMemo( () => cartItems - .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock) + .filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock) .reduce((sum, item) => { - const product = productsById[item.productId]; + const product = productsById[item.skuId]; const price = product?.price || 0; return sum + price * (quantities[item.id] || item.quantity); }, 0), @@ -208,9 +208,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { ); const totalPrice = cartItems - .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock) + .filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock) .reduce((sum, item) => { - const product = productsById[item.productId]; + const product = productsById[item.skuId]; const quantity = quantities[item.id] || item.quantity; const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0); return sum + Number(price) * quantity; @@ -282,7 +282,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { const finalTotalWithDelivery = finalTotal + deliveryCharge; - const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock); + const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.skuId]?.isOutOfStock); useEffect(() => { const initial: Record = {}; @@ -308,7 +308,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { if (isFlashDelivery) { newSelectedSlots[item.id] = 0; } else { - const productSlots = slotsData?.[item.productId]; + const productSlots = slotsData?.[item.skuId]; if (!productSlots || productSlots.length === 0) return; const now = dayjs(); @@ -416,11 +416,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { <> {cartItems.map((item, index) => { - const productSlots = getAvailableSlotsForProduct(item.productId); + const productSlots = getAvailableSlotsForProduct(item.skuId); const selectedSlotForItem = selectedSlots[item.id]; - const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true; - const product = productsById[item.productId]; - const productSlotInfo = productSlotsMap[item.productId]; + const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.skuId) : true; + const product = productsById[item.skuId]; + const productSlotInfo = productSlotsMap[item.skuId]; // const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible; let isAvailable = true; @@ -687,7 +687,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { {productSlotInfo?.isOutOfStock ? "Out of Stock" - : isFlashDelivery && !flashEligibleProductIds.has(item.productId) + : isFlashDelivery && !flashEligibleProductIds.has(item.skuId) ? "Not available for flash delivery. Please remove" : "No delivery slots available"} @@ -919,10 +919,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { onPress={() => { const availableItems = cartItems .filter(item => { - if (productSlotsMap[item.productId]?.isOutOfStock) return false; + if (productSlotsMap[item.skuId]?.isOutOfStock) return false; if (isFlashDelivery) { // Check if product supports flash delivery - return flashEligibleProductIds.has(item.productId); + return flashEligibleProductIds.has(item.skuId); } return selectedSlots[item.id]; // Regular delivery requires slot selection }) @@ -930,8 +930,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { if (availableItems.length === 0) { // Determine why no items are available - const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock); - const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock); + const outOfStockItems = cartItems.filter(item => productSlotsMap[item.skuId]?.isOutOfStock); + const inStockItems = cartItems.filter(item => !productSlotsMap[item.skuId]?.isOutOfStock); let errorTitle = "Cannot Proceed"; let errorMessage = ""; @@ -943,7 +943,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { } else if (isFlashDelivery) { // Check if any items are flash-eligible const flashEligibleItems = inStockItems.filter(item => - flashEligibleProductIds.has(item.productId) + flashEligibleProductIds.has(item.skuId) ); if (flashEligibleItems.length === 0) { errorTitle = "1 Hr Delivery Unavailable"; @@ -970,7 +970,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { // Check if there are items without slots (for regular delivery) if (!isFlashDelivery && availableItems.length < cartItems.length) { - const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock); + const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.skuId]?.isOutOfStock); if (itemsWithoutSlots.length > 0) { Alert.alert( "Delivery Slot Required", diff --git a/apps/user-ui/components/checkout-page.tsx b/apps/user-ui/components/checkout-page.tsx index f0ec290..637a5cc 100644 --- a/apps/user-ui/components/checkout-page.tsx +++ b/apps/user-ui/components/checkout-page.tsx @@ -90,7 +90,7 @@ const CheckoutPage: React.FC = ({ isFlashDelivery = false }) const selectedItems = cartItems.filter(item => { // For flash delivery, check if product supports flash delivery if (isFlashDelivery) { - return flashEligibleProductIds.has(item.productId); + return flashEligibleProductIds.has(item.skuId); } // For regular delivery, only include items with assigned slots return selectedSlots[item.id]; @@ -132,10 +132,10 @@ const CheckoutPage: React.FC = ({ isFlashDelivery = false }) const totalPrice = selectedItems - .filter((item) => !productSlotsMap[item.productId]?.isOutOfStock) + .filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock) .reduce( (sum, item) => { - const product = productsById[item.productId]; + const product = productsById[item.skuId]; const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0); return sum + price * item.quantity; }, diff --git a/apps/user-ui/components/floating-cart-bar.tsx b/apps/user-ui/components/floating-cart-bar.tsx index 044e055..717278e 100644 --- a/apps/user-ui/components/floating-cart-bar.tsx +++ b/apps/user-ui/components/floating-cart-bar.tsx @@ -122,17 +122,17 @@ const FloatingCartBar: React.FC = ({ const itemsToUpdate = cartItems.filter(item => { if (isFlashDelivery || !item.slotId) return false; - const availableSlots = productSlotsMap[item.productId]?.slots || []; + const availableSlots = productSlotsMap[item.skuId]?.slots || []; const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId); return !isSlotAvailable; }); itemsToUpdate.forEach((item) => { - const availableSlots = productSlotsMap[item.productId]?.slots || []; + const availableSlots = productSlotsMap[item.skuId]?.slots || []; if (availableSlots.length > 0 && !isFlashDelivery) { const nearestSlotId = availableSlots[0].id; removeFromCart.mutate({ itemId: item.id }); - addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId); + addToCartHook.addToCart(item.skuId, item.quantity, nearestSlotId); } }); }, []); @@ -143,7 +143,7 @@ const FloatingCartBar: React.FC = ({ // Calculate total cart value and free delivery info const totalCartValue = cartItems.reduce( (sum, item) => { - const product = productsById[item.productId]; + const product = productsById[item.skuId]; const basePrice = product?.price ?? 0; const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice; return sum + price * item.quantity; @@ -268,20 +268,20 @@ const FloatingCartBar: React.FC = ({ - {formatQuantity(productsById[item.productId]?.productQuantity || 1, productsById[item.productId]?.unitNotation || '').display} + {formatQuantity(productsById[item.skuId]?.productQuantity || 1, productsById[item.skuId]?.unitNotation || '').display} = ({ updateCartItem.mutate({ itemId: item.id, quantity: value }); } }} - step={productsById[item.productId]?.incrementStep || 1} + step={productsById[item.skuId]?.incrementStep || 1} showUnits={true} - // unit={productsById[item.productId]?.unitNotation} + // unit={productsById[item.skuId]?.unitNotation} /> - {item.slotId && slotsData && productSlotsMap[item.productId] && ( + {item.slotId && slotsData && productSlotsMap[item.skuId] && ( { + options={(productSlotsMap[item.skuId]?.slots || []).map((slot) => { return { label: slot ? formatTimeRange(slot.deliveryTime) : "N/A", value: slot.id, @@ -313,7 +313,7 @@ const FloatingCartBar: React.FC = ({ const newSlot = slotsData.slots.find(s => s.id === val); if (!newSlot) return; - const productId = item.productId; + const productId = item.skuId; const quantity = item.quantity; const itemId = item.id; const slotId = typeof val === 'number' ? val : Number(val); @@ -340,7 +340,7 @@ const FloatingCartBar: React.FC = ({ )} ₹{(() => { - const product = productsById[item.productId]; + const product = productsById[item.skuId]; const basePrice = product?.price ?? 0; const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice; return price * item.quantity; diff --git a/apps/user-ui/hooks/cart-query-hooks.tsx b/apps/user-ui/hooks/cart-query-hooks.tsx index b63bfda..09b7003 100644 --- a/apps/user-ui/hooks/cart-query-hooks.tsx +++ b/apps/user-ui/hooks/cart-query-hooks.tsx @@ -13,7 +13,7 @@ const getCartStorageKey = (cartType: CartType = "regular"): string => { interface LocalCartItem { id: number; - productId: number; + skuId: number; quantity: number; slotId: number; addedAt: string; @@ -35,7 +35,7 @@ interface ProductSummary { export interface CartItem { id: number; - productId: number; + skuId: number; quantity: number; addedAt: string; subtotal: number; @@ -66,7 +66,7 @@ interface UseGetCartReturn { } interface AddToCartVariables { - productId: number; + skuId: number; quantity: number; slotId: number; } @@ -94,8 +94,8 @@ interface UseAddToCartReturn { isLoading: boolean; error: Error | null; data: LocalCartItem[] | undefined; - addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void; - addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise; + addToCart: (skuId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void; + addToCartAsync: (skuId: number, quantity?: number, slotId?: number) => Promise; } interface UseUpdateCartItemReturn { @@ -135,9 +135,9 @@ const getNextCartItemId = (items: LocalCartItem[]): number => { return maxId + 1; }; -const addToLocalCart = async (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise => { +const addToLocalCart = async (skuId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise => { const items = await getLocalCart(cartType); - const existingIndex = items.findIndex(item => item.productId === productId); + const existingIndex = items.findIndex(item => item.skuId === skuId); if (existingIndex >= 0) { items[existingIndex].quantity += quantity; @@ -148,7 +148,7 @@ const addToLocalCart = async (productId: number, quantity: number, slotId: numbe const newId = getNextCartItemId(items); const cartItem: LocalCartItem = { id: newId, - productId, + skuId, quantity, slotId: slotId ?? 0, addedAt: new Date().toISOString(), @@ -211,14 +211,14 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType = const items: CartItem[] = cartItems .map((cartItem): CartItem | null => { - const productBasic = productMap[cartItem.productId]; - const productAvailability = productSlotsMap[cartItem.productId]; + const productBasic = productMap[cartItem.skuId]; + const productAvailability = productSlotsMap[cartItem.skuId]; if (!productBasic || !productAvailability) return null; return { id: cartItem.id, - productId: cartItem.productId, + skuId: cartItem.skuId, quantity: cartItem.quantity, addedAt: cartItem.addedAt, subtotal: Number(productBasic.price) * cartItem.quantity, @@ -256,8 +256,8 @@ export function useAddToCart(options: MutationOptions = useMutation({ - mutationFn: async ({ productId, quantity, slotId }: AddToCartVariables): Promise => { - return await addToLocalCart(productId, quantity, slotId, cartType); + mutationFn: async ({ skuId, quantity, slotId }: AddToCartVariables): Promise => { + return await addToLocalCart(skuId, quantity, slotId, cartType); }, onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => { queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] }); @@ -274,11 +274,11 @@ export function useAddToCart(options: MutationOptions void): void => { + const addToCart = (skuId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => { if (slotId == null) { throw new Error('slotId is required for adding to cart'); } - mutation.mutate({ productId, quantity, slotId }, { + mutation.mutate({ skuId, quantity, slotId }, { onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => { onSettled?.(data, error); } @@ -292,11 +292,11 @@ export function useAddToCart(options: MutationOptions => { + addToCartAsync: (skuId: number, quantity = 1, slotId?: number): Promise => { if (slotId == null) { throw new Error('slotId is required for adding to cart'); } - return mutation.mutateAsync({ productId, quantity, slotId }); + return mutation.mutateAsync({ skuId, quantity, slotId }); }, }; } diff --git a/apps/user-ui/src/components/AddToCartDialog.tsx b/apps/user-ui/src/components/AddToCartDialog.tsx index 9d72357..9bb9bd2 100644 --- a/apps/user-ui/src/components/AddToCartDialog.tsx +++ b/apps/user-ui/src/components/AddToCartDialog.tsx @@ -132,7 +132,7 @@ export default function AddToCartDialog() { } else { const slotId = selectedSlotId ?? availableSlotIds[0] ?? 0; addToCart.mutate( - { productId: product.id, quantity, slotId }, + { skuId: product.id, quantity, slotId }, { onSuccess: () => clearAddedToCartProduct() } ); } diff --git a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql index fcc01de..6d7c60b 100644 --- a/packages/db_helper_sqlite/drizzle/0002_sku_split.sql +++ b/packages/db_helper_sqlite/drizzle/0002_sku_split.sql @@ -222,7 +222,18 @@ ALTER TABLE `coupons` RENAME COLUMN `product_ids` TO `sku_ids`; ALTER TABLE `reserved_coupons` RENAME COLUMN `product_ids` TO `sku_ids`; ALTER TABLE `vendor_snippets` RENAME COLUMN `product_ids` TO `sku_ids`; --- 8. Clean up helper table. +-- 8. Update popularItems in key_val_store from product IDs to SKU IDs. +UPDATE `key_val_store` +SET `value` = ( + SELECT json_group_array(`m`.`sku_id`) + FROM json_each(`key_val_store`.`value`) AS `je` + JOIN `__product_to_sku` `m` ON `m`.`product_id` = `je`.`value` +) +WHERE `key` = 'popularItems' + AND `value` IS NOT NULL + AND `value` LIKE '[%'; + +-- 9. Clean up helper table. DROP TABLE `__product_to_sku`; -- PRAGMA foreign_keys=ON; diff --git a/packages/db_helper_sqlite/index.ts b/packages/db_helper_sqlite/index.ts index 789b16d..703feb7 100644 --- a/packages/db_helper_sqlite/index.ts +++ b/packages/db_helper_sqlite/index.ts @@ -311,7 +311,7 @@ export { cancelOrderTransaction as cancelUserOrderTransaction, updateOrderNotes as updateUserOrderNotes, getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds, - getProductIdsFromOrders as getUserProductIdsFromOrders, + getSkuIdsFromOrders as getUserProductIdsFromOrders, getProductsForRecentOrders as getUserProductsForRecentOrders, // Post-order handler helpers getOrdersByIdsWithFullData, @@ -364,7 +364,7 @@ export { // Common API Helpers export { - getSuspendedProductIds, + getSuspendedSkuIds, getNextDeliveryDateWithCapacity, } from './src/user-apis/product' diff --git a/packages/db_helper_sqlite/src/admin-apis/product.ts b/packages/db_helper_sqlite/src/admin-apis/product.ts index c2f5c10..a8102b6 100644 --- a/packages/db_helper_sqlite/src/admin-apis/product.ts +++ b/packages/db_helper_sqlite/src/admin-apis/product.ts @@ -792,12 +792,12 @@ export async function updateProductPrices(updates: Array<{ } const productIds = updates.map((update) => update.productId) - const existingProducts = await db.query.productInfo.findMany({ - where: inArray(productInfo.id, productIds), + const existingSkus = await db.query.productSkus.findMany({ + where: inArray(productSkus.id, productIds), columns: { id: true }, }) as Array<{ id: number }> - const existingIds = new Set(existingProducts.map((product: { id: number }) => product.id)) + const existingIds = new Set(existingSkus.map((sku: { id: number }) => sku.id)) const invalidIds = productIds.filter((id) => !existingIds.has(id)) if (invalidIds.length > 0) { @@ -806,7 +806,7 @@ export async function updateProductPrices(updates: Array<{ const updatePromises = updates.map((update) => { const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update - const updateData: Partial> = {} + const updateData: any = {} if (price !== undefined) updateData.price = price.toString() if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString() @@ -814,9 +814,9 @@ export async function updateProductPrices(updates: Array<{ if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable return db - .update(productInfo) + .update(productSkus) .set(updateData) - .where(eq(productInfo.id, productId)) + .where(eq(productSkus.id, productId)) }) await Promise.all(updatePromises) diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts index 4629d5c..90f5701 100644 --- a/packages/db_helper_sqlite/src/stores/store-helpers.ts +++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts @@ -5,7 +5,8 @@ import { db } from '../db/db_index' import { homeBanners, productInfo, - units, + productSkus, + skuFeatures, deliverySlotInfo, specialDeals, storeInfo, @@ -24,7 +25,7 @@ export interface BannerData { name: string imageUrl: string | null serialNum: number | null - productIds: number[] | null + skuIds: number[] | null createdAt: Date } @@ -41,7 +42,9 @@ export async function getAllBannersForCache(): Promise { export interface ProductBasicData { id: number + productId: number name: string + skuName: string | null shortDescription: string | null longDescription: string | null price: string @@ -49,7 +52,7 @@ export interface ProductBasicData { images: unknown isOutOfStock: boolean storeId: number | null - unitShortNotation: string + unitNotation: string incrementStep: number productQuantity: number isFlashAvailable: boolean @@ -63,7 +66,7 @@ export interface StoreBasicData { } export interface DeliverySlotData { - productId: number + skuId: number id: number deliveryTime: Date freezeTime: Date @@ -71,44 +74,64 @@ export interface DeliverySlotData { } export interface SpecialDealData { - productId: number + skuId: number quantity: string price: string validTill: Date } +export async function getAllSpecialDealsForCache(): Promise { + const results = await db + .select({ + skuId: specialDeals.skuId, + quantity: specialDeals.quantity, + price: specialDeals.price, + validTill: specialDeals.validTill, + }) + .from(specialDeals) + .where(gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`)) + + return results.map((deal) => ({ + ...deal, + quantity: String(deal.quantity ?? '0'), + price: String(deal.price ?? '0'), + })) +} + export interface ProductTagData { productId: number tagName: string } export async function getAllProductsForCache(): Promise { - const results = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - longDescription: productInfo.longDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - storeId: productInfo.storeId, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - productQuantity: productInfo.productQuantity, - isFlashAvailable: productInfo.isFlashAvailable, - flashPrice: productInfo.flashPrice, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) + const skus = await db.query.productSkus.findMany({ + with: { + product: true, + features: true, + }, + }) - return results.map((product) => ({ - ...product, - price: String(product.price ?? '0'), - marketPrice: product.marketPrice ? String(product.marketPrice) : null, - flashPrice: product.flashPrice ? String(product.flashPrice) : null, - })) + return skus.map((sku) => { + const features = sku.features || [] + return { + id: sku.id, + productId: sku.productId, + name: sku.product?.name ?? 'Unknown', + skuName: sku.name ?? null, + shortDescription: sku.product?.shortDescription ?? null, + longDescription: sku.product?.longDescription ?? null, + price: String(sku.price ?? '0'), + marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, + images: sku.images, + isOutOfStock: sku.isOutOfStock, + storeId: sku.product?.storeId ?? null, + unitNotation: features.map((f) => f.featureValue).join(' '), + incrementStep: sku.product?.incrementStep ?? 1, + productQuantity: 1, + isFlashAvailable: sku.isFlashAvailable, + flashPrice: sku.flashPrice ? String(sku.flashPrice) : null, + } + }) } export async function getAllStoresForCache(): Promise { @@ -126,13 +149,12 @@ export async function getAllDeliverySlotsForCache(): Promise ), }) - // Flatten slots with their product IDs const result: DeliverySlotData[] = [] for (const slot of slots) { - const productIds = slot.productIds || [] - for (const productId of productIds) { + const skuIds = slot.skuIds || [] + for (const skuId of skuIds) { result.push({ - productId, + skuId, id: slot.id, deliveryTime: slot.deliveryTime, freezeTime: slot.freezeTime, @@ -144,24 +166,6 @@ export async function getAllDeliverySlotsForCache(): Promise return result } -export async function getAllSpecialDealsForCache(): Promise { - const results = await db - .select({ - productId: specialDeals.productId, - quantity: specialDeals.quantity, - price: specialDeals.price, - validTill: specialDeals.validTill, - }) - .from(specialDeals) - .where(gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`)) - - return results.map((deal) => ({ - ...deal, - quantity: String(deal.quantity ?? '0'), - price: String(deal.price ?? '0'), - })) -} - export async function getAllProductTagsForCache(): Promise { return db .select({ @@ -224,23 +228,26 @@ export interface SlotWithProductsData { isCapacityFull: boolean products: Array<{ id: number + productId: number name: string + skuName: string | null productQuantity: number shortDescription: string | null price: string marketPrice: string | null - unit: { shortNotation: string } | null + unitNotation: string store: { id: number; name: string; description: string | null } | null images: unknown isOutOfStock: boolean storeId: number | null + isFlashAvailable: boolean + flashPrice: string | null }> } export async function getAllSlotsWithProductsForCache(): Promise { const now = new Date() - // Get all active future slots const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), @@ -249,68 +256,64 @@ export async function getAllSlotsWithProductsForCache(): Promise() + const allSkuIds = new Set() for (const slot of slots) { - for (const productId of (slot.productIds || [])) { - allProductIds.add(productId) + for (const skuId of (slot.skuIds || [])) { + allSkuIds.add(skuId) } } - // Fetch all products in one query - const productIdsArray = Array.from(allProductIds) - const productIdSet = new Set(productIdsArray); - // const productsData = productIdsArray.length > 0 - // ? await db.query.productInfo.findMany({ - // where: inArray(productInfo.id, productIdsArray), - // with: { - // unit: true, - // store: true, - // }, - // }) - // : [] - let productsData = productIdsArray.length > 0 - ? await db.query.productInfo.findMany({ - // where: inArray(productInfo.id, productIdsArray), - with: { - unit: true, - store: true, + const skuIdsArray = Array.from(allSkuIds) + const skuIdSet = new Set(skuIdsArray) + + let skusData: any[] = [] + if (skuIdsArray.length > 0) { + skusData = await db.query.productSkus.findMany({ + with: { + product: { + with: { store: true }, }, - }) - : [] + features: true, + }, + }) + skusData = skusData.filter((item: any) => skuIdSet.has(item.id)) + } - productsData = productsData.filter(item => productIdSet.has(item.id)) + const skuMap = new Map(skusData.map((s: any) => [s.id, s])) - // Create a map for quick lookup - const productMap = new Map(productsData.map(p => [p.id, p])) - - // Build the result - return slots.map(slot => ({ + return slots.map((slot) => ({ id: slot.id, deliveryTime: slot.deliveryTime, freezeTime: slot.freezeTime, isActive: slot.isActive, isCapacityFull: slot.isCapacityFull, - products: (slot.productIds || []) - .map(productId => productMap.get(productId)) + products: (slot.skuIds || []) + .map((skuId: number) => skuMap.get(skuId)) .filter((p): p is NonNullable => p != null) - .map(product => ({ - id: product.id, - name: product.name, - productQuantity: product.productQuantity, - shortDescription: product.shortDescription, - price: String(product.price ?? '0'), - marketPrice: product.marketPrice ? String(product.marketPrice) : null, - unit: product.unit ? { shortNotation: product.unit.shortNotation } : null, - store: product.store ? { - id: product.store.id, - name: product.store.name, - description: product.store.description - } : null, - images: product.images, - isOutOfStock: product.isOutOfStock, - storeId: product.storeId, - })), + .map((sku: any) => { + const features = sku.features || [] + return { + id: sku.id, + productId: sku.productId, + name: sku.product?.name ?? 'Unknown', + skuName: sku.name ?? null, + productQuantity: 1, + shortDescription: sku.product?.shortDescription ?? null, + price: String(sku.price ?? '0'), + marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, + unitNotation: features.map((f: any) => f.featureValue).join(' '), + store: sku.product?.store ? { + id: sku.product.store.id, + name: sku.product.store.name, + description: sku.product.store.description + } : null, + images: sku.images, + isOutOfStock: sku.isOutOfStock, + storeId: sku.product?.storeId ?? null, + isFlashAvailable: sku.isFlashAvailable, + flashPrice: sku.flashPrice ? String(sku.flashPrice) : null, + } + }), })) as SlotWithProductsData[] } diff --git a/packages/db_helper_sqlite/src/user-apis/order.ts b/packages/db_helper_sqlite/src/user-apis/order.ts index 81d31c6..acc4bc9 100644 --- a/packages/db_helper_sqlite/src/user-apis/order.ts +++ b/packages/db_helper_sqlite/src/user-apis/order.ts @@ -5,6 +5,7 @@ import { orderStatus, addresses, productInfo, + productSkus, paymentInfoTable, coupons, couponUsage, @@ -258,9 +259,9 @@ export async function getAddressByIdAndUser( }) } -export async function getProductById(productId: number) { - return db.query.productInfo.findFirst({ - where: eq(productInfo.id, productId), +export async function getProductById(skuId: number) { + return db.query.productSkus.findFirst({ + where: eq(productSkus.id, skuId), }) } @@ -341,12 +342,12 @@ export async function placeOrderTransaction(params: { export async function deleteCartItemsForOrder( userId: number, - productIds: number[] + skuIds: number[] ): Promise { await db.delete(cartItems).where( and( eq(cartItems.userId, userId), - inArray(cartItems.productId, productIds) + inArray(cartItems.skuId, skuIds) ) ) } @@ -375,10 +376,16 @@ export async function getOrdersWithRelations( with: { orderItems: { with: { - product: { + sku: { + with: { + product: { + columns: { + name: true, + }, + }, + }, columns: { id: true, - name: true, images: true, }, }, @@ -451,10 +458,16 @@ export async function getOrderByIdWithRelations( with: { orderItems: { with: { - product: { + sku: { + with: { + product: { + columns: { + name: true, + }, + }, + }, columns: { id: true, - name: true, images: true, }, }, @@ -610,15 +623,15 @@ export async function getRecentlyDeliveredOrderIds( return recentOrders.map((order) => order.id) } -export async function getProductIdsFromOrders( +export async function getSkuIdsFromOrders( orderIds: number[] ): Promise { const orderItemsResult = await db - .select({ productId: orderItems.productId }) + .select({ skuId: orderItems.skuId }) .from(orderItems) .where(inArray(orderItems.orderId, orderIds)) - return [...new Set(orderItemsResult.map((item) => item.productId))] + return [...new Set(orderItemsResult.map((item) => item.skuId))] } export interface RecentProductData { @@ -714,14 +727,17 @@ export async function getOrdersByIdsWithFullData( }, orderItems: { with: { - product: { + sku: { columns: { name: true, - productQuantity: true, + price: true, }, with: { - unit: true - } + product: { + columns: { name: true }, + }, + features: true, + }, }, }, }, diff --git a/packages/db_helper_sqlite/src/user-apis/product.ts b/packages/db_helper_sqlite/src/user-apis/product.ts index 3373d19..900b7e4 100644 --- a/packages/db_helper_sqlite/src/user-apis/product.ts +++ b/packages/db_helper_sqlite/src/user-apis/product.ts @@ -1,5 +1,5 @@ import { db } from '../db/db_index' -import { deliverySlotInfo, productInfo, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema' +import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema' import { and, desc, eq, gt, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' @@ -8,42 +8,27 @@ const getStringArray = (value: unknown): string[] | null => { return value.map((item) => String(item)) } -export async function getProductDetailById(productId: number): Promise { - const productData = await db - .select({ - id: productInfo.id, - name: productInfo.name, - shortDescription: productInfo.shortDescription, - longDescription: productInfo.longDescription, - price: productInfo.price, - marketPrice: productInfo.marketPrice, - images: productInfo.images, - isOutOfStock: productInfo.isOutOfStock, - storeId: productInfo.storeId, - unitShortNotation: units.shortNotation, - incrementStep: productInfo.incrementStep, - productQuantity: productInfo.productQuantity, - isFlashAvailable: productInfo.isFlashAvailable, - flashPrice: productInfo.flashPrice, - }) - .from(productInfo) - .innerJoin(units, eq(productInfo.unitId, units.id)) - .where(eq(productInfo.id, productId)) - .limit(1) +export async function getProductDetailById(skuId: number): Promise { + const sku = await db.query.productSkus.findFirst({ + where: eq(productSkus.id, skuId), + with: { + product: true, + features: true, + }, + }) - if (productData.length === 0) { + if (!sku) { return null } - const product = productData[0] + const features = sku.features || [] + const product = sku.product - const storeData = product.storeId ? await db.query.storeInfo.findFirst({ + const storeData = product?.storeId ? await db.query.storeInfo.findFirst({ where: eq(storeInfo.id, product.storeId), columns: { id: true, name: true, description: true }, }) : null - // Note: deliverySlots are now fetched from cache in the frontend via useSlots() - // This avoids expensive database joins on every product detail view const specialDealsData = await db .select({ quantity: specialDeals.quantity, @@ -53,32 +38,32 @@ export async function getProductDetailById(productId: number): Promise f.featureValue).join(' '), + images: getStringArray(sku.images), + isOutOfStock: sku.isOutOfStock, store: storeData ? { id: storeData.id, name: storeData.name, description: storeData.description ?? null, } : null, - incrementStep: product.incrementStep, - productQuantity: product.productQuantity, - isFlashAvailable: product.isFlashAvailable, - flashPrice: product.flashPrice?.toString() || null, - deliverySlots: [], // Fetched from cache in frontend via useSlots() + incrementStep: product?.incrementStep ?? 1, + productQuantity: 1, + isFlashAvailable: sku.isFlashAvailable, + flashPrice: sku.flashPrice?.toString() || null, + deliverySlots: [], specialDeals: specialDealsData.map((deal) => ({ quantity: String(deal.quantity ?? '0'), price: String(deal.price ?? '0'), @@ -126,9 +111,9 @@ export async function getProductReviews(productId: number, limit: number, offset } } -export async function getProductById(productId: number) { - return db.query.productInfo.findFirst({ - where: eq(productInfo.id, productId), +export async function getProductById(skuId: number) { + return db.query.productSkus.findFirst({ + where: eq(productSkus.id, skuId), }) } @@ -221,20 +206,20 @@ export async function getAllProductsWithUnits(tagId?: number): Promise { - const suspendedProducts = await db - .select({ id: productInfo.id }) - .from(productInfo) - .where(eq(productInfo.isSuspended, true)) +export async function getSuspendedSkuIds(): Promise { + const suspendedSkus = await db + .select({ id: productSkus.id }) + .from(productSkus) + .where(eq(productSkus.isSuspended, true)) - return suspendedProducts.map(sp => sp.id) + return suspendedSkus.map(sp => sp.id) } /** * Get next delivery date for a product (with capacity check) * This version filters by both isActive AND isCapacityFull */ -export async function getNextDeliveryDateWithCapacity(productId: number): Promise { +export async function getNextDeliveryDateWithCapacity(skuId: number): Promise { const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), @@ -244,10 +229,9 @@ export async function getNextDeliveryDateWithCapacity(productId: number): Promis orderBy: desc(deliverySlotInfo.deliveryTime), }) - // Find the first slot that contains this product for (const slot of slots) { - const productIds = slot.productIds || [] - if (productIds.includes(productId)) { + const skuIds = slot.skuIds || [] + if (skuIds.includes(skuId)) { return slot.deliveryTime } } diff --git a/packages/db_helper_sqlite/src/user-apis/slots.ts b/packages/db_helper_sqlite/src/user-apis/slots.ts index 4863a46..8ad89ce 100644 --- a/packages/db_helper_sqlite/src/user-apis/slots.ts +++ b/packages/db_helper_sqlite/src/user-apis/slots.ts @@ -1,5 +1,5 @@ import { db } from '../db/db_index' -import { deliverySlotInfo, productInfo } from '../db/schema' +import { deliverySlotInfo, productSkus } from '../db/schema' import { asc, eq } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm' import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared' @@ -27,20 +27,17 @@ export async function getActiveSlotsList(): Promise { } export async function getProductAvailability(): Promise { - const products = await db - .select({ - id: productInfo.id, - name: productInfo.name, - isOutOfStock: productInfo.isOutOfStock, - isFlashAvailable: productInfo.isFlashAvailable, - }) - .from(productInfo) - .where(eq(productInfo.isSuspended, false)) + const skus = await db.query.productSkus.findMany({ + where: eq(productSkus.isSuspended, false), + with: { + product: { columns: { name: true } }, + }, + }) - return products.map((product) => ({ - id: product.id, - name: product.name, - isOutOfStock: product.isOutOfStock, - isFlashAvailable: product.isFlashAvailable, + return skus.map((sku) => ({ + id: sku.id, + name: sku.product?.name ?? 'Unknown', + isOutOfStock: sku.isOutOfStock, + isFlashAvailable: sku.isFlashAvailable, })) } diff --git a/packages/db_helper_sqlite/src/user-apis/stores.ts b/packages/db_helper_sqlite/src/user-apis/stores.ts index 076b4ce..9383afb 100644 --- a/packages/db_helper_sqlite/src/user-apis/stores.ts +++ b/packages/db_helper_sqlite/src/user-apis/stores.ts @@ -1,22 +1,10 @@ import { db } from '../db/db_index' -import { productInfo, storeInfo, units } from '../db/schema' -import { and, eq, sql } from 'drizzle-orm' +import { productInfo, productSkus, storeInfo } from '../db/schema' +import { and, eq, inArray, sql } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm' import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared' type StoreRow = InferSelectModel -type StoreProductRow = { - id: number - name: string - shortDescription: string | null - price: string | null - marketPrice: string | null - images: unknown - isOutOfStock: boolean - incrementStep: number - unitShortNotation: string - productQuantity: number -} const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null @@ -24,32 +12,59 @@ const getStringArray = (value: unknown): string[] | null => { } export async function getStoreSummaries(): Promise { + // Count SKUs per store, filtering by suspended SKUs const storesData = await db .select({ id: storeInfo.id, name: storeInfo.name, description: storeInfo.description, imageUrl: storeInfo.imageUrl, - productCount: sql`count(${productInfo.id})`.as('productCount'), + productCount: sql`count(${productSkus.id})`.as('productCount'), }) .from(storeInfo) .leftJoin( productInfo, - and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false)) + eq(productInfo.storeId, storeInfo.id) + ) + .leftJoin( + productSkus, + and( + eq(productSkus.productId, productInfo.id), + eq(productSkus.isSuspended, false) + ) ) .groupBy(storeInfo.id) const storesWithDetails = await Promise.all( storesData.map(async (store) => { - const sampleProducts = await db - .select({ - id: productInfo.id, - name: productInfo.name, - images: productInfo.images, - }) - .from(productInfo) - .where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false))) - .limit(3) + let sampleProducts: any[] = [] + // Get sample SKUs from this store + if (store.productCount > 0) { + const storeProductIds = await db + .select({ id: productInfo.id }) + .from(productInfo) + .where(eq(productInfo.storeId, store.id)) + + const productIdArr = storeProductIds.map((p) => p.id) + if (productIdArr.length > 0) { + const skus = await db.query.productSkus.findMany({ + where: and( + inArray(productSkus.productId, productIdArr), + eq(productSkus.isSuspended, false) + ), + with: { + product: { columns: { name: true } }, + }, + columns: { id: true, images: true, name: true }, + limit: 3, + }) + sampleProducts = skus.map((sku) => ({ + id: sku.id, + name: sku.product?.name ?? sku.name ?? 'Unknown', + images: getStringArray(sku.images), + })) + } + } return { id: store.id, @@ -57,11 +72,7 @@ export async function getStoreSummaries(): Promise { description: store.description ?? null, imageUrl: store.imageUrl ?? null, productCount: store.productCount || 0, - sampleProducts: sampleProducts.map((product) => ({ - id: product.id, - name: product.name, - images: getStringArray(product.images), - })), + sampleProducts, } }) ) @@ -84,36 +95,42 @@ export async function getStoreDetail(storeId: number): Promise ({ - id: product.id, - name: product.name, - shortDescription: product.shortDescription ?? null, - price: String(product.price ?? '0'), - marketPrice: product.marketPrice ? String(product.marketPrice) : null, - incrementStep: product.incrementStep, - unit: product.unitShortNotation, - unitNotation: product.unitShortNotation, - images: getStringArray(product.images), - isOutOfStock: product.isOutOfStock, - productQuantity: product.productQuantity, - })) + const productIdArr = storeProductIds.map((p) => p.id) + + const skus = productIdArr.length > 0 + ? await db.query.productSkus.findMany({ + where: and( + inArray(productSkus.productId, productIdArr), + eq(productSkus.isSuspended, false) + ), + with: { + product: true, + features: true, + }, + }) + : [] + + const products: UserStoreProductData[] = skus.map((sku) => { + const features = sku.features || [] + return { + id: sku.id, + name: sku.product?.name ?? 'Unknown', + shortDescription: sku.product?.shortDescription ?? null, + price: String(sku.price ?? '0'), + marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, + incrementStep: sku.product?.incrementStep ?? 1, + unit: features.map((f) => f.featureValue).join(' '), + unitNotation: features.map((f) => f.featureValue).join(' '), + images: getStringArray(sku.images), + isOutOfStock: sku.isOutOfStock, + productQuantity: 1, + } + }) return { store: { @@ -126,10 +143,6 @@ export async function getStoreDetail(storeId: number): Promise { return db.query.storeInfo.findMany({ columns: { diff --git a/packages/shared/types/user.ts b/packages/shared/types/user.ts index f45bd16..2c19277 100644 --- a/packages/shared/types/user.ts +++ b/packages/shared/types/user.ts @@ -121,7 +121,7 @@ export interface UserBanner { name: string; imageUrl: string; description: string | null; - productIds: number[] | null; + skuIds: number[] | null; redirectUrl: string | null; serialNum: number | null; isActive: boolean;