freshyo/packages/db_helper_sqlite/src/user-apis/product.ts
2026-09-06 18:47:21 +05:30

393 lines
12 KiB
TypeScript

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<UserProductDetailData | null> {
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<UserProductReview> {
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<ProductSummaryData[]> {
const taggedProductIdSet = new Set<number>()
// 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<number[]> {
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<Date | null> {
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<SkuSummary[]> {
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<OffersPageData> {
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 }
}