import { db } from '../db/db_index' import { productInfo, productMarketStats, 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' // Chunk sizes for multi-row inserts so no single statement exceeds SQLite's // 999-bind-parameter limit (chunk size × params-per-row must stay under 999). const SKU_INSERT_CHUNK_SIZE = 100 // 6 params per SKU row const FEATURE_INSERT_CHUNK_SIZE = 200 // 3 params per feature row const COMBO_ITEM_INSERT_CHUNK_SIZE = 400 // 2 params per combo-item row // product_combos has a UNIQUE index on (comboSkuId, skuId) — drop duplicates // (by skuId) before inserting so a repeated pick doesn't fail the insert. const dedupeComboItems = (items: any[]): any[] => { const seen = new Set() return (items || []).filter((ci: any) => { const id = Number(ci.skuId) if (!id || seen.has(id)) return false seen.add(id) return true }) } import type { AdminProduct, AdminProductGroupInfo, AdminProductTagInfo, AdminProductTagWithProducts, AdminProductReview, AdminProductWithDetails, AdminProductWithRelations, AdminSku, AdminSkuFeature, AdminSpecialDeal, AdminUnit, AdminUpdateSlotProductsResult, CreateComboItemInput, CreateProductInput, CreateSkuInput, ProductTagCore, SkuFeatureLike, Store, } from '@packages/shared' type ProductRow = InferSelectModel type SkuRow = InferSelectModel type MarketStatsRow = InferSelectModel type SkuFeatureRow = InferSelectModel type StoreRow = InferSelectModel type SpecialDealRow = InferSelectModel type ProductTagInfoRow = InferSelectModel type ProductTagRow = InferSelectModel const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null return value.map((item) => String(item)) } 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[] = [], marketStats: MarketStatsRow | null = null ): AdminSku => ({ id: sku.id, productId: sku.productId, name: sku.name ?? null, price: marketStats?.ourPrice ? String(marketStats.ourPrice) : '0', marketPrice: marketStats?.marketPrice ? String(marketStats.marketPrice) : null, images: getStringArray(sku.images), imageKeys: getStringArray(sku.images), isOutOfStock: marketStats?.isOutOfStock ?? false, isSuspended: marketStats?.isSuspended ?? false, isFlashAvailable: marketStats?.isFlashAvailable ?? false, flashPrice: marketStats?.flashPrice ? String(marketStats.flashPrice) : null, isOffer: sku.isOffer, isComboOnly: sku.isComboOnly, isDeleted: sku.isDeleted ?? false, 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, sortOrder: tag.sortOrder ?? [], 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, marketStats: true, comboItems: { with: { sku: { with: { product: true, features: true, marketStats: 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: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0', })) return mapSku(sku, sku.features, comboItems, sku.marketStats) }), })) } 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, marketStats: true, comboItems: { with: { sku: { with: { product: true, features: true, marketStats: 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: any) => ({ featureName: f.featureName, featureValue: f.featureValue })), productName: ci.sku?.product?.name ?? 'Unknown', images: getStringArray(ci.sku?.images), price: ci.sku?.marketStats?.ourPrice ? String(ci.sku.marketStats.ourPrice) : '0', })) return mapSku(sku, sku.features, comboItems, sku.marketStats) }) 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 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') } try { return await db.transaction(async (tx) => { const { skus, ...productData } = input const [product] = await tx.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 skuChunks = await runBatched(tx, skus, SKU_INSERT_CHUNK_SIZE, async (t, chunk) => { return t.insert(productSkus).values( chunk.map((sku) => ({ productId: product.id, name: sku.name ?? null, images: sku.images ?? null, isOffer: sku.isOffer ?? false, isComboOnly: sku.isComboOnly ?? false, isDeleted: sku.isDeleted ?? false, })) ).returning() }) const skuRows = skuChunks.flat() for (let i = 0; i < skuRows.length; i++) { const skuRow = skuRows[i] const sku = skus[i] await tx.insert(productMarketStats).values({ skuId: skuRow.id, marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, ourPrice: sku.price != null ? String(sku.price) : '0', isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, isOutOfStock: sku.isOutOfStock ?? false, isSuspended: sku.isSuspended ?? false, }) if (sku.features && sku.features.length > 0) { await runBatched(tx, sku.features, FEATURE_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(skuFeatures).values( chunk.map((f) => ({ skuId: skuRow.id, featureName: f.featureName, featureValue: f.featureValue, })) ) }) } if (sku.comboItems && sku.comboItems.length > 0) { const comboItems = dedupeComboItems(sku.comboItems) if (comboItems.length > 0) { await runBatched(tx, comboItems, COMBO_ITEM_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(productCombos).values( chunk.map((ci: any) => ({ comboSkuId: skuRow.id, skuId: ci.skuId, })) ) }) } } } const createdSkus = await tx.query.productSkus.findMany({ where: eq(productSkus.productId, product.id), with: { features: true, marketStats: true }, }) return { ...mapProduct(product), store: null, skus: createdSkus.map((s) => mapSku(s, s.features, [], s.marketStats)), } }) } catch (error) { console.error('createProduct failed:', error) throw new Error('Failed to create product. Please check the SKU details and try again.') } } 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 // Input validation outside the transaction so specific messages reach the UI. 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)) const skusBeingSuspended = skus.filter( (sku: any) => sku.isSuspended && sku.id != null && existingSkuIdSet.has(sku.id) ) if (skusBeingSuspended.length > 0) { const suspendingIds = skusBeingSuspended.map((sku: any) => sku.id) const comboMemberships = await db.query.productCombos.findMany({ where: inArray(productCombos.skuId, suspendingIds), columns: { comboSkuId: true }, }) const comboIds = Array.from(new Set(comboMemberships.map((c) => c.comboSkuId))) if (comboIds.length > 0) { const combos = await db.query.productMarketStats.findMany({ where: inArray(productMarketStats.skuId, comboIds), columns: { skuId: true, isSuspended: true }, }) const activeComboIds = combos.filter((c) => !c.isSuspended).map((c) => c.skuId) if (activeComboIds.length > 0) { throw new Error( `Cannot suspend SKU(s): they are members of combo(s) ${activeComboIds.join(', ')} that are not suspended` ) } } } } try { return await db.transaction(async (tx) => { await tx.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) { const existingSkus = await tx.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 tx.update(productSkus) .set({ name: sku.name ?? null, images: sku.images ?? null, isOffer: sku.isOffer ?? false, isComboOnly: sku.isComboOnly ?? false, ...(sku.isDeleted !== undefined && { isDeleted: sku.isDeleted }), }) .where(eq(productSkus.id, sku.id)) const existingMarketStats = await tx.query.productMarketStats.findFirst({ where: eq(productMarketStats.skuId, sku.id), columns: { id: true }, }) const marketStatsValues = { marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, ourPrice: sku.price != null ? String(sku.price) : '0', isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, isOutOfStock: sku.isOutOfStock ?? false, isSuspended: sku.isSuspended ?? false, } if (existingMarketStats) { await tx.update(productMarketStats) .set(marketStatsValues) .where(eq(productMarketStats.skuId, sku.id)) } else { await tx.insert(productMarketStats).values({ skuId: sku.id, ...marketStatsValues, }) } await tx.delete(skuFeatures).where(eq(skuFeatures.skuId, sku.id)) if (sku.features && sku.features.length > 0) { await runBatched(tx, sku.features, FEATURE_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(skuFeatures).values( chunk.map((f: any) => ({ skuId: sku.id, featureName: f.featureName, featureValue: f.featureValue, })) ) }) } await tx.delete(productCombos).where(eq(productCombos.comboSkuId, sku.id)) if (sku.comboItems && sku.comboItems.length > 0) { const comboItems = dedupeComboItems(sku.comboItems) if (comboItems.length > 0) { await runBatched(tx, comboItems, COMBO_ITEM_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(productCombos).values( chunk.map((ci: any) => ({ comboSkuId: sku.id, skuId: ci.skuId, })) ) }) } } } else { // Insert new SKU const [newSku] = await tx.insert(productSkus).values({ productId: id, name: sku.name ?? null, images: sku.images ?? null, isOffer: sku.isOffer ?? false, isComboOnly: sku.isComboOnly ?? false, isDeleted: sku.isDeleted ?? false, }).returning() await tx.insert(productMarketStats).values({ skuId: newSku.id, marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null, ourPrice: sku.price != null ? String(sku.price) : '0', isFlashAvailable: sku.isFlashAvailable ?? false, flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null, isOutOfStock: sku.isOutOfStock ?? false, isSuspended: sku.isSuspended ?? false, }) if (sku.features && sku.features.length > 0) { await runBatched(tx, sku.features, FEATURE_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(skuFeatures).values( chunk.map((f: any) => ({ skuId: newSku.id, featureName: f.featureName, featureValue: f.featureValue, })) ) }) } // New SKUs also save their combo items (with dedupe). if (sku.comboItems && sku.comboItems.length > 0) { const comboItems = dedupeComboItems(sku.comboItems) if (comboItems.length > 0) { await runBatched(tx, comboItems, COMBO_ITEM_INSERT_CHUNK_SIZE, async (t, chunk) => { await t.insert(productCombos).values( chunk.map((ci: any) => ({ comboSkuId: newSku.id, skuId: ci.skuId, })) ) }) } } } } } const updatedProduct = await tx.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { store: true, skus: { with: { features: true, marketStats: true }, }, }, }) if (!updatedProduct) { return null } return { ...mapProduct(updatedProduct), store: updatedProduct.store ? mapStore(updatedProduct.store) : null, skus: updatedProduct.skus.map((s) => mapSku(s, s.features, [], s.marketStats)), } }) } catch (error) { console.error('updateProduct failed:', error) throw new Error('Failed to update product. Please check the SKU details and try again.') } } 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 getAllProductTags(): Promise { const tags = await db.query.productTagInfo.findMany({ with: { products: { with: { sku: { with: { features: true, marketStats: true }, }, }, }, }, }) as Array }> return tags.map((tag: any) => ({ ...mapTagInfo(tag), products: tag.products.map((assignment: any) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats), })), productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId), })) } 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 extends ProductTagCore { sortOrder?: 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 || [], sortOrder: input.sortOrder || [], }).returning() return { ...mapTagInfo(tag), products: [], productIds: [], } } export async function getProductTagById(tagId: number): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { sku: { with: { features: true, marketStats: true }, }, }, }, }, }) if (!tag) { return null } return { ...mapTagInfo(tag), products: (tag.products || []).map((assignment: any) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats), })), productIds: (tag.products || []).map((assignment: ProductTagRow) => assignment.productId), } } export type UpdateProductTagInput = Omit & { tagName?: string sortOrder?: 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 }), ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }), }).where(eq(productTagInfo.id, tagId)).returning() const fullTag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { sku: { with: { features: true, marketStats: true }, }, }, }, }, }) return { ...mapTagInfo(tag), products: (fullTag?.products || []).map((assignment: any) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats), })), productIds: (fullTag?.products || []).map((assignment: ProductTagRow) => assignment.productId) || [], } } 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: { sku: { with: { features: true, marketStats: 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) => mapSku(membership.sku, membership.sku.features, [], membership.sku.marketStats) ), 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 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.ourPrice = 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 if (Object.keys(updateData).length === 0) continue const existingMarketStats = await tx.query.productMarketStats.findFirst({ where: eq(productMarketStats.skuId, productId), columns: { id: true }, }) if (existingMarketStats) { await tx .update(productMarketStats) .set(updateData) .where(eq(productMarketStats.skuId, productId)) } else { await tx.insert(productMarketStats).values({ skuId: productId, ourPrice: updateData.ourPrice ?? '0', marketPrice: updateData.marketPrice ?? null, flashPrice: updateData.flashPrice ?? null, isFlashAvailable: updateData.isFlashAvailable ?? false, }) } } }) 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 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 replaceTagProducts(tagId: number, productIds: number[]): Promise { await db.delete(productTags).where(eq(productTags.tagId, tagId)) if (productIds.length === 0) { return } const productAssociations = productIds.map((productId) => ({ productId, tagId, })) await db.insert(productTags).values(productAssociations) }