import { db } from '../db/db_index' import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership, productTagInfo, users, storeInfo, } from '../db/schema' import { and, desc, eq, inArray, sql } from 'drizzle-orm' import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' import type { AdminProduct, AdminProductGroupInfo, AdminProductTagInfo, AdminProductTagWithProducts, AdminProductReview, AdminProductWithDetails, AdminProductWithRelations, AdminSpecialDeal, AdminUnit, AdminUpdateSlotProductsResult, Store, } from '@packages/shared' type ProductRow = InferSelectModel type UnitRow = InferSelectModel type StoreRow = InferSelectModel type SpecialDealRow = InferSelectModel type ProductTagInfoRow = InferSelectModel type ProductTagRow = InferSelectModel type ProductGroupRow = InferSelectModel type ProductGroupMembershipRow = InferSelectModel type ProductReviewRow = InferSelectModel const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null return value.map((item) => String(item)) } const mapUnit = (unit: UnitRow): AdminUnit => ({ id: unit.id, shortNotation: unit.shortNotation, fullName: unit.fullName, }) const mapStore = (store: StoreRow): Store => ({ id: store.id, name: store.name, description: store.description, imageUrl: store.imageUrl, owner: store.owner, createdAt: store.createdAt, // updatedAt: store.createdAt, }) const mapProduct = (product: ProductRow): AdminProduct => ({ id: product.id, name: product.name, shortDescription: product.shortDescription ?? null, longDescription: product.longDescription ?? null, unitId: product.unitId, price: String(product.price ?? '0'), marketPrice: product.marketPrice ? String(product.marketPrice) : null, images: getStringArray(product.images), imageKeys: getStringArray(product.images), isOutOfStock: product.isOutOfStock, isSuspended: product.isSuspended, isFlashAvailable: product.isFlashAvailable, flashPrice: product.flashPrice ? String(product.flashPrice) : null, createdAt: product.createdAt, incrementStep: product.incrementStep, productQuantity: product.productQuantity, storeId: product.storeId, }) const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({ id: deal.id, productId: deal.productId, quantity: String(deal.quantity ?? '0'), price: String(deal.price ?? '0'), validTill: deal.validTill, }) const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({ id: tag.id, tagName: tag.tagName, tagDescription: tag.tagDescription ?? null, imageUrl: tag.imageUrl ?? null, isDashboardTag: tag.isDashboardTag, relatedStores: tag.relatedStores, createdAt: tag.createdAt, }) export async function getAllProducts(): Promise { type ProductWithRelationsRow = ProductRow & { unit: UnitRow; store: StoreRow | null } const products = await db.query.productInfo.findMany({ orderBy: productInfo.name, with: { unit: true, store: true, }, }) as ProductWithRelationsRow[] return products.map((product) => ({ ...mapProduct(product), unit: mapUnit(product.unit), store: product.store ? mapStore(product.store) : null, })) } export async function getProductById(id: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { unit: true, }, }) if (!product) { return null } const deals = await db.query.specialDeals.findMany({ where: eq(specialDeals.productId, id), orderBy: specialDeals.quantity, }) const productTagsData = await db.query.productTags.findMany({ where: eq(productTags.productId, id), with: { tag: true, }, }) as Array return { ...mapProduct(product), unit: mapUnit(product.unit), deals: deals.map(mapSpecialDeal), tags: productTagsData.map((tag) => mapTagInfo(tag.tag)), } } export async function deleteProduct(id: number): Promise { const [deletedProduct] = await db .delete(productInfo) .where(eq(productInfo.id, id)) .returning() if (!deletedProduct) { return null } return mapProduct(deletedProduct) } type ProductInfoInsert = InferInsertModel type ProductInfoUpdate = Partial export async function createProduct(input: ProductInfoInsert): Promise { const productQuantityRaw = (input as any).productQuantity const productQuantity = typeof productQuantityRaw === 'string' ? Number(productQuantityRaw) : productQuantityRaw const safeProductQuantity = typeof productQuantity === 'number' && Number.isFinite(productQuantity) ? productQuantity : 1 const [product] = await db.insert(productInfo).values({ ...input, productQuantity: safeProductQuantity, }).returning() return mapProduct(product) } export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise { const productQuantityRaw = (updates as any).productQuantity const productQuantity = typeof productQuantityRaw === 'string' ? Number(productQuantityRaw) : productQuantityRaw const safeUpdates = typeof productQuantityRaw === 'undefined' ? updates : { ...updates, productQuantity: typeof productQuantity === 'number' && Number.isFinite(productQuantity) ? productQuantity : 1, } const [product] = await db.update(productInfo) .set(safeUpdates) .where(eq(productInfo.id, id)) .returning() if (!product) { return null } return mapProduct(product) } export async function toggleProductOutOfStock(id: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), }) if (!product) { return null } const [updatedProduct] = await db .update(productInfo) .set({ isOutOfStock: !product.isOutOfStock, }) .where(eq(productInfo.id, id)) .returning() if (!updatedProduct) { return null } return mapProduct(updatedProduct) } export async function updateSlotProducts(slotId: string, productIds: string[]): Promise { const currentAssociations = await db.query.productSlots.findMany({ where: eq(productSlots.slotId, parseInt(slotId)), columns: { productId: true, }, }) as Array<{ productId: number }> const currentProductIds = currentAssociations.map((assoc: { productId: number }) => assoc.productId) const newProductIds = productIds.map((id: string) => parseInt(id)) const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id)) const productsToRemove = currentProductIds.filter((id: number) => !newProductIds.includes(id)) if (productsToRemove.length > 0) { await db.delete(productSlots).where( and( eq(productSlots.slotId, parseInt(slotId)), inArray(productSlots.productId, productsToRemove) ) ) } if (productsToAdd.length > 0) { const newAssociations = productsToAdd.map((productId) => ({ productId, slotId: parseInt(slotId), })) await db.insert(productSlots).values(newAssociations) } return { message: 'Slot products updated successfully', added: productsToAdd.length, removed: productsToRemove.length, } } export async function getSlotProductIds(slotId: string): Promise { const associations = await db.query.productSlots.findMany({ where: eq(productSlots.slotId, parseInt(slotId)), columns: { productId: true, }, }) return associations.map((assoc: { productId: number }) => assoc.productId) } export async function getAllUnits(): Promise { const allUnits = await db.query.units.findMany({ orderBy: units.shortNotation, }) return allUnits.map(mapUnit) } export async function getAllProductTags(): Promise { const tags = await db.query.productTagInfo.findMany({ with: { products: { with: { product: true, }, }, }, }) as Array }> return tags.map((tag: ProductTagInfoRow & { products: Array }) => ({ ...mapTagInfo(tag), products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })), })) } export async function getAllProductTagInfos(): Promise { const tags = await db.query.productTagInfo.findMany({ orderBy: productTagInfo.tagName, }) return tags.map(mapTagInfo) } export async function getProductTagInfoById(tagId: number): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), }) if (!tag) { return null } return mapTagInfo(tag) } export interface CreateProductTagInput { tagName: string tagDescription?: string | null imageUrl?: string | null isDashboardTag?: boolean relatedStores?: number[] } export async function createProductTag(input: CreateProductTagInput): Promise { const [tag] = await db.insert(productTagInfo).values({ tagName: input.tagName, tagDescription: input.tagDescription || null, imageUrl: input.imageUrl || null, isDashboardTag: input.isDashboardTag || false, relatedStores: input.relatedStores || [], }).returning() return { ...mapTagInfo(tag), products: [], } } export async function getProductTagById(tagId: number): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { product: true, }, }, }, }) if (!tag) { return null } return { ...mapTagInfo(tag), products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })), } } export interface UpdateProductTagInput { tagName?: string tagDescription?: string | null imageUrl?: string | null isDashboardTag?: boolean relatedStores?: number[] } export async function updateProductTag(tagId: number, input: UpdateProductTagInput): Promise { const [tag] = await db.update(productTagInfo).set({ ...(input.tagName !== undefined && { tagName: input.tagName }), ...(input.tagDescription !== undefined && { tagDescription: input.tagDescription }), ...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }), ...(input.isDashboardTag !== undefined && { isDashboardTag: input.isDashboardTag }), ...(input.relatedStores !== undefined && { relatedStores: input.relatedStores }), }).where(eq(productTagInfo.id, tagId)).returning() const fullTag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { product: true, }, }, }, }) return { ...mapTagInfo(tag), products: fullTag?.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })) || [], } } export async function deleteProductTag(tagId: number): Promise { await db.delete(productTagInfo).where(eq(productTagInfo.id, tagId)) } export async function checkProductTagExistsByName(tagName: string): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.tagName, tagName), }) return !!tag } export async function getSlotsProductIds(slotIds: number[]): Promise> { if (slotIds.length === 0) { return {} } const associations = await db.query.productSlots.findMany({ where: inArray(productSlots.slotId, slotIds), columns: { slotId: true, productId: true, }, }) as Array<{ slotId: number; productId: number }> const result: Record = {} for (const assoc of associations) { if (!result[assoc.slotId]) { result[assoc.slotId] = [] } result[assoc.slotId].push(assoc.productId) } slotIds.forEach((slotId) => { if (!result[slotId]) { result[slotId] = [] } }) return result } 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, adminResponse: productReviews.adminResponse, adminResponseImages: productReviews.adminResponseImages, 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: AdminProductReview[] = reviews.map((review: any) => ({ id: review.id, reviewBody: review.reviewBody, ratings: review.ratings, imageUrls: review.imageUrls, reviewTime: review.reviewTime, adminResponse: review.adminResponse ?? null, adminResponseImages: review.adminResponseImages, userName: review.userName ?? null, })) return { reviews: mappedReviews, totalCount, } } export async function respondToReview( reviewId: number, adminResponse: string | undefined, adminResponseImages: string[] ): Promise { const [updatedReview] = await db .update(productReviews) .set({ adminResponse, adminResponseImages, }) .where(eq(productReviews.id, reviewId)) .returning() if (!updatedReview) { return null } return { id: updatedReview.id, reviewBody: updatedReview.reviewBody, ratings: updatedReview.ratings, imageUrls: updatedReview.imageUrls, reviewTime: updatedReview.reviewTime, adminResponse: updatedReview.adminResponse ?? null, adminResponseImages: updatedReview.adminResponseImages, userName: null, } } export async function getAllProductGroups() { const groups = await db.query.productGroupInfo.findMany({ with: { memberships: { with: { product: true, }, }, }, orderBy: desc(productGroupInfo.createdAt), }) return groups.map((group: any) => ({ id: group.id, groupName: group.groupName, description: group.description ?? null, createdAt: group.createdAt, products: group.memberships.map((membership: any) => mapProduct(membership.product)), productCount: group.memberships.length, memberships: group.memberships })) } export async function createProductGroup( groupName: string, description: string | undefined, productIds: number[] ): Promise { const [newGroup] = await db .insert(productGroupInfo) .values({ groupName, description, }) .returning() if (productIds.length > 0) { const memberships = productIds.map((productId) => ({ productId, groupId: newGroup.id, })) await db.insert(productGroupMembership).values(memberships) } return { id: newGroup.id, groupName: newGroup.groupName, description: newGroup.description ?? null, createdAt: newGroup.createdAt, } } export async function updateProductGroup( id: number, groupName: string | undefined, description: string | undefined, productIds: number[] | undefined ): Promise { const updateData: Partial<{ groupName: string description: string | null }> = {} if (groupName !== undefined) updateData.groupName = groupName if (description !== undefined) updateData.description = description const [updatedGroup] = await db .update(productGroupInfo) .set(updateData) .where(eq(productGroupInfo.id, id)) .returning() if (!updatedGroup) { return null } if (productIds !== undefined) { await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)) if (productIds.length > 0) { const memberships = productIds.map((productId) => ({ productId, groupId: id, })) await db.insert(productGroupMembership).values(memberships) } } return { id: updatedGroup.id, groupName: updatedGroup.groupName, description: updatedGroup.description ?? null, createdAt: updatedGroup.createdAt, } } export async function deleteProductGroup(id: number): Promise { await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)) const [deletedGroup] = await db .delete(productGroupInfo) .where(eq(productGroupInfo.id, id)) .returning() if (!deletedGroup) { return null } return { id: deletedGroup.id, groupName: deletedGroup.groupName, description: deletedGroup.description ?? null, createdAt: deletedGroup.createdAt, } } export async function addProductToGroup(groupId: number, productId: number): Promise { await db.insert(productGroupMembership).values({ groupId, productId }) } export async function removeProductFromGroup(groupId: number, productId: number): Promise { await db.delete(productGroupMembership) .where(and( eq(productGroupMembership.groupId, groupId), eq(productGroupMembership.productId, productId) )) } export async function updateProductPrices(updates: Array<{ productId: number price?: number marketPrice?: number | null flashPrice?: number | null isFlashAvailable?: boolean }>) { if (updates.length === 0) { return { updatedCount: 0, invalidIds: [] } } const productIds = updates.map((update) => update.productId) const existingProducts = await db.query.productInfo.findMany({ where: inArray(productInfo.id, productIds), columns: { id: true }, }) as Array<{ id: number }> const existingIds = new Set(existingProducts.map((product: { id: number }) => product.id)) const invalidIds = productIds.filter((id) => !existingIds.has(id)) if (invalidIds.length > 0) { return { updatedCount: 0, invalidIds } } const updatePromises = updates.map((update) => { const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update const updateData: Partial> = {} if (price !== undefined) updateData.price = price.toString() if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString() if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString() if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable return db .update(productInfo) .set(updateData) .where(eq(productInfo.id, productId)) }) await Promise.all(updatePromises) return { updatedCount: updates.length, invalidIds: [] } } // ========================================================================== // Product Helpers for Admin Controller // ========================================================================== export async function checkProductExistsByName(name: string): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.name, name), columns: { id: true }, }) return !!product } export async function checkUnitExists(unitId: number): Promise { const unit = await db.query.units.findFirst({ where: eq(units.id, unitId), columns: { id: true }, }) return !!unit } export async function getProductImagesById(productId: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, productId), columns: { images: true }, }) if (!product) { return null } return getStringArray(product.images) || [] } export interface CreateSpecialDealInput { quantity: number price: number validTill: string | Date } export async function createSpecialDealsForProduct( productId: number, deals: CreateSpecialDealInput[] ): Promise { if (deals.length === 0) { return [] } const dealInserts = deals.map((deal) => ({ productId, quantity: deal.quantity.toString(), price: deal.price.toString(), validTill: new Date(deal.validTill), })) const createdDeals = await db .insert(specialDeals) .values(dealInserts) .returning() return createdDeals.map(mapSpecialDeal) } export async function updateProductDeals( productId: number, deals: CreateSpecialDealInput[] ): Promise { if (deals.length === 0) { await db.delete(specialDeals).where(eq(specialDeals.productId, productId)) return } const existingDeals = await db.query.specialDeals.findMany({ where: eq(specialDeals.productId, productId), }) const existingDealsMap = new Map( existingDeals.map((deal: SpecialDealRow) => [`${deal.quantity}-${deal.price}`, deal]) ) const newDealsMap = new Map( deals.map((deal) => [`${deal.quantity}-${deal.price}`, deal]) ) const dealsToAdd = deals.filter((deal) => { const key = `${deal.quantity}-${deal.price}` return !existingDealsMap.has(key) }) const dealsToRemove = existingDeals.filter((deal: SpecialDealRow) => { const key = `${deal.quantity}-${deal.price}` return !newDealsMap.has(key) }) const dealsToUpdate = deals.filter((deal: CreateSpecialDealInput) => { const key = `${deal.quantity}-${deal.price}` const existing = existingDealsMap.get(key) const nextValidTill = deal.validTill instanceof Date ? deal.validTill.toISOString().split('T')[0] : String(deal.validTill) return existing && existing.validTill.toISOString().split('T')[0] !== nextValidTill }) if (dealsToRemove.length > 0) { await db.delete(specialDeals).where( inArray(specialDeals.id, dealsToRemove.map((deal: SpecialDealRow) => deal.id)) ) } if (dealsToAdd.length > 0) { const dealInserts = dealsToAdd.map((deal) => ({ productId, quantity: deal.quantity.toString(), price: deal.price.toString(), validTill: new Date(deal.validTill), })) await db.insert(specialDeals).values(dealInserts) } for (const deal of dealsToUpdate) { const key = `${deal.quantity}-${deal.price}` const existingDeal = existingDealsMap.get(key) if (existingDeal) { await db.update(specialDeals) .set({ validTill: new Date(deal.validTill) }) .where(eq(specialDeals.id, existingDeal.id)) } } } export async function replaceProductTags(productId: number, tagIds: number[]): Promise { await db.delete(productTags).where(eq(productTags.productId, productId)) if (tagIds.length === 0) { return } const tagAssociations = tagIds.map((tagId) => ({ productId, tagId, })) await db.insert(productTags).values(tagAssociations) }