// @ts-nocheck -- temporary: file partially updated for SKU migration; remaining functions to be fixed later import { db } from '../db/db_index' import { productInfo, productSkus, skuFeatures, productCombos, units, specialDeals, deliverySlotInfo, productTags, productReviews, productGroupInfo, productGroupMembership, productTagInfo, users, storeInfo, cartItems, orderItems, coupons, reservedCoupons, vendorSnippets, homeBanners, keyValStore, couponApplicableProducts, } from '../db/schema' import { and, desc, eq, inArray, sql } from 'drizzle-orm' import { runBatched } from '../lib/run-batched' import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' import type { AdminProduct, AdminProductGroupInfo, AdminProductTagInfo, AdminProductTagWithProducts, AdminProductReview, AdminProductWithDetails, AdminProductWithRelations, AdminSku, AdminSkuFeature, AdminSpecialDeal, AdminUnit, AdminUpdateSlotProductsResult, Store, } from '@packages/shared' type ProductRow = InferSelectModel type SkuRow = InferSelectModel type SkuFeatureRow = 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, storeId: product.storeId, incrementStep: product.incrementStep, createdAt: product.createdAt, productType: product.productType as 'item' | 'combo', }) const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({ id: feature.id, skuId: feature.skuId, featureName: feature.featureName, featureValue: feature.featureValue, }) const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] = []): AdminSku => ({ id: sku.id, productId: sku.productId, name: sku.name ?? null, price: String(sku.price ?? '0'), marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, images: getStringArray(sku.images), imageKeys: getStringArray(sku.images), isOutOfStock: sku.isOutOfStock, isSuspended: sku.isSuspended, isFlashAvailable: sku.isFlashAvailable, flashPrice: sku.flashPrice ? String(sku.flashPrice) : null, createdAt: sku.createdAt, features: features.map(mapSkuFeature), comboItems: comboItems, }) const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({ id: deal.id, skuId: deal.skuId, 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 & { store: StoreRow | null skus: Array } const products = await db.query.productInfo.findMany({ orderBy: productInfo.name, with: { store: true, skus: { with: { features: true, comboItems: { with: { sku: { with: { product: true, features: true } }, }, }, }, }, }, }) as ProductWithRelationsRow[] return products.map((product) => ({ ...mapProduct(product), store: product.store ? mapStore(product.store) : null, skus: product.skus.map((sku) => { const comboItems = (sku.comboItems || []).map((ci: any) => ({ skuId: ci.skuId, skuName: ci.sku?.name ?? null, features: (ci.sku?.features || []).map((f: any) => ({ featureName: f.featureName, featureValue: f.featureValue })), productName: ci.sku?.product?.name ?? 'Unknown', images: getStringArray(ci.sku?.images), price: String(ci.sku?.price ?? '0'), })) return mapSku(sku, sku.features, comboItems) }), })) } export async function getProductById(id: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { store: true, skus: { with: { features: true, comboItems: { with: { sku: { with: { product: true, features: true } }, }, }, }, }, }, }) if (!product) { return null } const skuIds = product.skus.map((sku) => sku.id) const deals = skuIds.length > 0 ? await db.query.specialDeals.findMany({ where: inArray(specialDeals.skuId, skuIds), orderBy: specialDeals.quantity, }) : [] const productTagsData = await db.query.productTags.findMany({ where: eq(productTags.productId, id), with: { tag: true, }, }) as Array const skusWithCombos = product.skus.map((sku) => { const comboItems = (sku.comboItems || []).map((ci: any) => ({ skuId: ci.skuId, skuName: ci.sku?.name ?? null, features: (ci.sku?.features || []).map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })), productName: ci.sku?.product?.name ?? 'Unknown', images: getStringArray(ci.sku?.images), price: String(ci.sku?.price ?? '0'), })) return mapSku(sku, sku.features, comboItems) }) return { ...mapProduct(product), store: product.store ? mapStore(product.store) : null, skus: skusWithCombos, 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: CreateProductInput): Promise { if (!input.skus || input.skus.length === 0) { throw new Error('At least one SKU is required') } const featuresHaveQuantity = input.skus.every((sku) => sku.features.some((f) => f.featureName === 'quantity') ) if (!featuresHaveQuantity) { throw new Error('Every SKU must have a quantity feature') } const { skus, ...productData } = input const [product] = await db.insert(productInfo).values({ name: productData.name, shortDescription: productData.shortDescription ?? null, longDescription: productData.longDescription ?? null, storeId: productData.storeId ?? null, incrementStep: productData.incrementStep ?? 1, productType: productData.productType ?? 'item', }).returning() const skuRows = await db.insert(productSkus).values( skus.map((sku) => ({ productId: product.id, name: sku.name ?? null, price: String(sku.price), marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, images: sku.images ?? null, isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, })) ).returning() for (let i = 0; i < skuRows.length; i++) { const skuRow = skuRows[i] const sku = skus[i] await db.insert(skuFeatures).values( sku.features.map((f) => ({ skuId: skuRow.id, featureName: f.featureName, featureValue: f.featureValue, })) ) if (sku.comboItems && sku.comboItems.length > 0) { await db.insert(productCombos).values( sku.comboItems.map((ci: any) => ({ comboSkuId: skuRow.id, skuId: ci.skuId, })) ) } } const createdSkus = await db.query.productSkus.findMany({ where: eq(productSkus.productId, product.id), with: { features: true }, }) return { ...mapProduct(product), store: null, skus: createdSkus.map((s) => mapSku(s, s.features)), } } export async function updateProduct(id: number, input: any): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), }) if (!product) { return null } const { skus, ...productData } = input await db.update(productInfo) .set({ name: productData.name, shortDescription: productData.shortDescription ?? null, longDescription: productData.longDescription ?? null, storeId: productData.storeId ?? null, incrementStep: productData.incrementStep ?? 1, productType: productData.productType, }) .where(eq(productInfo.id, id)) if (skus !== undefined) { if (skus.length === 0) { throw new Error('At least one SKU is required') } const featuresHaveQuantity = skus.every((sku: any) => sku.features.some((f: any) => f.featureName === 'quantity') ) if (!featuresHaveQuantity) { throw new Error('Every SKU must have a quantity feature') } const existingSkus = await db.query.productSkus.findMany({ where: eq(productSkus.productId, id), columns: { id: true }, }) const existingSkuIdSet = new Set(existingSkus.map((s) => s.id)) for (const sku of skus) { if (sku.id != null && existingSkuIdSet.has(sku.id)) { // Update existing SKU await db.update(productSkus) .set({ name: sku.name ?? null, price: String(sku.price), marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, images: sku.images ?? null, isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, }) .where(eq(productSkus.id, sku.id)) await db.delete(skuFeatures).where(eq(skuFeatures.skuId, sku.id)) await db.insert(skuFeatures).values( sku.features.map((f: any) => ({ skuId: sku.id, featureName: f.featureName, featureValue: f.featureValue, })) ) await db.delete(productCombos).where(eq(productCombos.comboSkuId, sku.id)) if (sku.comboItems && sku.comboItems.length > 0) { await db.insert(productCombos).values( sku.comboItems.map((ci: any) => ({ comboSkuId: sku.id, skuId: ci.skuId, })) ) } } else { // Insert new SKU const [newSku] = await db.insert(productSkus).values({ productId: id, name: sku.name ?? null, price: String(sku.price), marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, images: sku.images ?? null, isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, }).returning() await db.insert(skuFeatures).values( sku.features.map((f: any) => ({ skuId: newSku.id, featureName: f.featureName, featureValue: f.featureValue, })) ) } } } const updatedProduct = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { store: true, skus: { with: { features: true }, }, }, }) if (!updatedProduct) { return null } return { ...mapProduct(updatedProduct), store: updatedProduct.store ? mapStore(updatedProduct.store) : null, skus: updatedProduct.skus.map((s) => mapSku(s, s.features)), } } export async function updateSlotProducts(slotId: string, productIds: string[]): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, parseInt(slotId)), }) if (!slot) { throw new Error(`Slot ${slotId} not found`) } const currentSkuIds = slot.skuIds || [] const newSkuIds = productIds.map((id: string) => parseInt(id)) await db.update(deliverySlotInfo) .set({ skuIds: newSkuIds }) .where(eq(deliverySlotInfo.id, parseInt(slotId))) const productsToAdd = newSkuIds.filter((id: number) => !currentSkuIds.includes(id)) const productsToRemove = currentSkuIds.filter((id: number) => !newSkuIds.includes(id)) return { message: 'Slot products updated successfully', added: productsToAdd.length, removed: productsToRemove.length, } } export async function getSlotProductIds(slotId: string): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, parseInt(slotId)), }) return slot?.skuIds || [] } 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 slots = await db.query.deliverySlotInfo.findMany({ where: inArray(deliverySlotInfo.id, slotIds), }) const result: Record = {} for (const slot of slots) { result[slot.id] = slot.skuIds || [] } 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) // Validate all SKU IDs exist (in chunks to avoid large IN clauses) const existingSkuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => { return tx.query.productSkus.findMany({ where: inArray(productSkus.id, chunk), columns: { id: true }, }) }) const existingIds = new Set(existingSkuChunks.flat().map((sku: { id: number }) => sku.id)) const invalidIds = productIds.filter((id) => !existingIds.has(id)) if (invalidIds.length > 0) { return { updatedCount: 0, invalidIds } } // Apply updates in chunks inside a single transaction await runBatched(db, updates, 10, async (tx, chunk) => { for (const update of chunk) { const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update const updateData: any = {} 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 await tx .update(productSkus) .set(updateData) .where(eq(productSkus.id, productId)) } }) 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.productSkus.findFirst({ where: eq(productSkus.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 createSpecialDealsForSku( 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 updateSkuDeals( productId: number, deals: CreateSpecialDealInput[] ): Promise { if (deals.length === 0) { await db.delete(specialDeals).where(eq(specialDeals.skuId, productId)) return } const existingDeals = await db.query.specialDeals.findMany({ where: eq(specialDeals.skuId, 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) } export async function mergeSkus(fromSkuId: number, toSkuId: number) { if (fromSkuId === toSkuId) { return { fromSkuId, toSkuId, message: 'SKUs are the same, no merge needed', counts: {} } } const fromSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, fromSkuId) }) if (!fromSku) throw new Error(`SKU ${fromSkuId} not found`) const toSku = await db.query.productSkus.findFirst({ where: eq(productSkus.id, toSkuId) }) if (!toSku) throw new Error(`SKU ${toSkuId} not found`) const counts: Record = {} // 1. order_items — direct update const orderItemsResult = await db.update(orderItems) .set({ skuId: toSkuId }) .where(eq(orderItems.skuId, fromSkuId)) counts.orderItems = orderItemsResult.changes ?? 0 // 2. special_deals — direct update const specialDealsResult = await db.update(specialDeals) .set({ skuId: toSkuId }) .where(eq(specialDeals.skuId, fromSkuId)) counts.specialDeals = specialDealsResult.changes ?? 0 // 3. cart_items — delete all with fromSkuId const cartResult = await db.delete(cartItems) .where(eq(cartItems.skuId, fromSkuId)) counts.cartItems = cartResult.changes ?? 0 // 4. coupon_applicable_products — update, but handle unique constraint // First delete rows where (coupon_id, toSkuId) already exists const existingCapRows = await db.query.couponApplicableProducts.findMany({ where: eq(couponApplicableProducts.skuId, toSkuId), columns: { couponId: true }, }) const existingCouponIds = new Set(existingCapRows.map((r) => r.couponId)) if (existingCouponIds.size > 0) { const dupResult = await db.delete(couponApplicableProducts) .where( and( eq(couponApplicableProducts.skuId, fromSkuId), inArray(couponApplicableProducts.couponId, Array.from(existingCouponIds)) ) ) counts.couponDedupDeleted = dupResult.changes ?? 0 } // Now update remaining rows const capResult = await db.update(couponApplicableProducts) .set({ skuId: toSkuId }) .where(eq(couponApplicableProducts.skuId, fromSkuId)) counts.couponApplicable = capResult.changes ?? 0 // 5. JSON arrays — remap fromSkuId to toSkuId const jsonTables: Array<{ table: any; column: string; name: string }> = [ { table: deliverySlotInfo, column: 'skuIds', name: 'deliverySlotInfo' }, { table: homeBanners, column: 'skuIds', name: 'homeBanners' }, { table: coupons, column: 'skuIds', name: 'coupons' }, { table: reservedCoupons, column: 'skuIds', name: 'reservedCoupons' }, { table: vendorSnippets, column: 'skuIds', name: 'vendorSnippets' }, ] for (const { table, column, name } of jsonTables) { const rows = await db.select({ id: table.id, ids: table[column] }).from(table) let updated = 0 for (const row of rows) { const ids: number[] = (row.ids as number[]) || [] if (!ids.includes(fromSkuId)) continue const newIds = ids.map((id) => (id === fromSkuId ? toSkuId : id)) const deduped = [...new Set(newIds)] if (deduped.length !== ids.length || deduped.some((id, i) => id !== ids[i])) { await db.update(table).set({ [column]: deduped } as any).where(eq(table.id, row.id)) updated++ } } counts[name] = updated } // popularItems in key_val_store const kvRow = await db.query.keyValStore.findFirst({ where: eq(keyValStore.key, 'popularItems'), }) if (kvRow && kvRow.value) { try { const arr: number[] = JSON.parse(kvRow.value) if (arr.includes(fromSkuId)) { const newArr = [...new Set(arr.map((id) => (id === fromSkuId ? toSkuId : id)))] await db.update(keyValStore) .set({ value: JSON.stringify(newArr) }) .where(eq(keyValStore.key, 'popularItems')) counts.popularItems = 1 } } catch { /* value not valid JSON, skip */ } } // 6. Delete SKU features and the SKU itself const featuresResult = await db.delete(skuFeatures).where(eq(skuFeatures.skuId, fromSkuId)) counts.skuFeatures = featuresResult.changes ?? 0 const skuResult = await db.delete(productSkus).where(eq(productSkus.id, fromSkuId)) counts.productSkus = skuResult.changes ?? 0 // 7. Delete orphaned product const remainingSkus = await db.query.productSkus.findMany({ where: eq(productSkus.productId, fromSku.productId), columns: { id: true }, }) let orphanedProductId: number | undefined if (remainingSkus.length === 0) { await db.delete(productInfo).where(eq(productInfo.id, fromSku.productId)) orphanedProductId = fromSku.productId counts.orphanedProduct = 1 } return { fromSkuId, toSkuId, orphanedProductId, counts, } }