import { db } from '../db/db_index' import { deliverySlotInfo, productInfo, productCombos, productSkus, productMarketStats, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema' import { and, desc, eq, gt, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' import { composeSkuName, composeUnitNotation } from '../lib/sku-features' const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null return value.map((item) => String(item)) } export async function getProductDetailById(skuId: number): Promise { const sku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, skuId), with: { product: true, features: true, marketStats: true, }, }) if (!sku) { return null } if (sku.marketStats?.isSuspended || sku.isDeleted) { return null } const features = sku.features || [] const product = sku.product const marketStats = sku.marketStats const storeData = product?.storeId ? await db.query.storeInfo.findFirst({ where: eq(storeInfo.id, product.storeId), columns: { id: true, name: true, description: true }, }) : null const specialDealsData = await db .select({ quantity: specialDeals.quantity, price: specialDeals.price, validTill: specialDeals.validTill, }) .from(specialDeals) .where( and( eq(specialDeals.skuId, skuId), gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`) ) ) .orderBy(specialDeals.quantity) const comboItemsData = await db.query.productCombos.findMany({ where: eq(productCombos.comboSkuId, skuId), with: { sku: { with: { product: true, features: true, marketStats: true } }, }, }) const comboItems = comboItemsData.map((ci) => { const ciFeatures = ci.sku?.features || [] return { skuId: ci.skuId, skuName: ci.sku?.name ?? null, unitNotation: composeUnitNotation(ciFeatures), productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', ciFeatures, ci.sku?.name), images: getStringArray(ci.sku?.images), price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0', isOffer: ci.sku?.isOffer ?? false, } }) return { id: sku.id, productId: sku.productId, name: composeSkuName(product?.name ?? 'Unknown', features, sku.name), shortDescription: product?.shortDescription ?? null, longDescription: product?.longDescription ?? null, price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0', marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null, unitNotation: composeUnitNotation(features), images: getStringArray(sku.images), isOutOfStock: marketStats?.isOutOfStock ?? false, store: storeData ? { id: storeData.id, name: storeData.name, description: storeData.description ?? null, } : null, incrementStep: product?.incrementStep ?? 1, productQuantity: 1, isFlashAvailable: marketStats?.isFlashAvailable ?? false, flashPrice: marketStats?.flashPrice?.toString() || null, deliverySlots: [], specialDeals: specialDealsData.map((deal) => ({ quantity: String(deal.quantity ?? '0'), price: String(deal.price ?? '0'), validTill: deal.validTill, })), productType: product?.productType ?? 'item', comboItems, } } export async function getProductReviews(productId: number, limit: number, offset: number) { const reviews = await db .select({ id: productReviews.id, reviewBody: productReviews.reviewBody, ratings: productReviews.ratings, imageUrls: productReviews.imageUrls, reviewTime: productReviews.reviewTime, userName: users.name, }) .from(productReviews) .innerJoin(users, eq(productReviews.userId, users.id)) .where(eq(productReviews.productId, productId)) .orderBy(desc(productReviews.reviewTime)) .limit(limit) .offset(offset) const totalCountResult = await db .select({ count: sql`count(*)` }) .from(productReviews) .where(eq(productReviews.productId, productId)) const totalCount = Number(totalCountResult[0].count) const mappedReviews: UserProductReview[] = reviews.map((review) => ({ id: review.id, reviewBody: review.reviewBody, ratings: review.ratings, imageUrls: getStringArray(review.imageUrls), reviewTime: review.reviewTime, userName: review.userName ?? null, })) return { reviews: mappedReviews, totalCount, } } export async function getProductById(skuId: number) { const sku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, skuId), with: { marketStats: true, product: true, }, }) if (!sku) { return null } if (sku.isDeleted) { return null } const marketStats = sku.marketStats return { ...sku, price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0', marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null, flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null, isFlashAvailable: marketStats?.isFlashAvailable ?? false, isOutOfStock: marketStats?.isOutOfStock ?? false, } } export async function createProductReview( userId: number, productId: number, reviewBody: string, ratings: number, imageUrls: string[] ): Promise { const [newReview] = await db.insert(productReviews).values({ userId, productId, reviewBody, ratings, imageUrls, }).returning({ id: productReviews.id, reviewBody: productReviews.reviewBody, ratings: productReviews.ratings, imageUrls: productReviews.imageUrls, reviewTime: productReviews.reviewTime, }) return { id: newReview.id, reviewBody: newReview.reviewBody, ratings: newReview.ratings, imageUrls: getStringArray(newReview.imageUrls), reviewTime: newReview.reviewTime, userName: null, } } export interface ProductSummaryData { id: number name: string skuId: number skuName: string | null shortDescription: string | null price: string marketPrice: string | null images: unknown isOutOfStock: boolean unitShortNotation: string productQuantity: number features: { featureName: string | null; featureValue: string }[] } export async function getAllProductsWithUnits(tagId?: number): Promise { const taggedProductIdSet = new Set() // If tagId is provided, get product IDs that have this tag if (tagId) { const taggedProducts = await db .select({ productId: productTags.productId }) .from(productTags) .where(eq(productTags.tagId, tagId)) for (const tp of taggedProducts) { taggedProductIdSet.add(tp.productId) } } const skus = await db.query.productSkus.findMany({ with: { product: true, features: true, marketStats: true, }, }) return skus .filter((sku) => { if (sku.marketStats?.isSuspended) return false if (sku.isDeleted) return false if (!tagId) return true // product_tags.product_id now holds SKU ids — compare against sku.id return taggedProductIdSet.has(sku.id) }) .map((sku) => { const features = sku.features || [] const marketStats = sku.marketStats return { id: sku.product?.id ?? 0, name: composeSkuName(sku.product?.name ?? 'Unknown', features, sku.name), skuId: sku.id, skuName: sku.name ?? null, shortDescription: sku.product?.shortDescription ?? null, price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0', marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null, images: sku.images, isOutOfStock: marketStats?.isOutOfStock ?? false, unitShortNotation: composeUnitNotation(features), productQuantity: 1, features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })), } }) } /** * Get all suspended product IDs */ export async function getSuspendedSkuIds(): Promise { const suspendedSkus = await db .select({ id: productMarketStats.skuId }) .from(productMarketStats) .where(eq(productMarketStats.isSuspended, true)) 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(skuId: number): Promise { const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isCapacityFull, false), gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) ), orderBy: desc(deliverySlotInfo.deliveryTime), }) for (const slot of slots) { const skuIds = slot.skuIds || [] if (skuIds.includes(skuId)) { return slot.deliveryTime } } return null } // Single source in @packages/shared (admin.ts); re-exported for module compat. import type { ProductSummaryCore, SkuSummary } from '@packages/shared' export type { SkuSummary } from '@packages/shared' export async function getAllSkusSummary(): Promise { const skus = await db.query.productSkus.findMany({ with: { features: true, marketStats: true, product: { columns: { name: true, storeId: true }, }, }, }) return skus .filter((sku) => !sku.marketStats?.isSuspended && !sku.isDeleted) .map((sku) => { const featureValues = (sku.features || []).map((f) => f.featureValue) const label = [sku.product?.name ?? 'Unknown', ...featureValues].join(' ') return { id: sku.id, productId: sku.productId, productName: sku.product?.name ?? 'Unknown', label, images: sku.images, storeId: sku.product?.storeId ?? null, price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0', } }) } export interface OffersPageProductData extends ProductSummaryCore { } export interface OffersPageData { combos: OffersPageProductData[] offers: OffersPageProductData[] } const mapOffersPageProduct = (sku: { id: number marketStats: { ourPrice: string | null marketPrice: string | null isOutOfStock: boolean } | null images: unknown name: string | null product: { name: string; incrementStep: number | null } | null features: Array<{ featureValue: string }> }): OffersPageProductData => { const features = sku.features || [] return { id: sku.id, name: composeSkuName(sku.product?.name ?? 'Unknown', features, sku.name), price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0', marketPrice: sku.marketStats?.marketPrice ? String(sku.marketStats.marketPrice) : null, unitNotation: composeUnitNotation(features), images: (sku.images ?? null) as string[] | null, isOutOfStock: sku.marketStats?.isOutOfStock ?? false, incrementStep: sku.product?.incrementStep ?? 1, } } export async function getOffersAndCombos(): Promise { const skus = await db.query.productSkus.findMany({ with: { product: true, features: true, marketStats: true, }, }) const combos: OffersPageProductData[] = [] const offers: OffersPageProductData[] = [] for (const sku of skus) { if (sku.marketStats?.isSuspended) continue if (sku.isDeleted) continue if (sku.product?.productType === 'combo') { combos.push(mapOffersPageProduct(sku)) } if (sku.isOffer) { offers.push(mapOffersPageProduct(sku)) } } return { combos, offers } }