import { db } from '../db/db_index' import { deliverySlotInfo, productInfo, productSkus, skuFeatures, vendorSnippets, productGroupInfo, } from '../db/schema' import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm' import type { AdminDeliverySlot, AdminSlotWithProducts, AdminSlotWithProductsAndSnippetsBase, AdminSlotCreateResult, AdminSlotUpdateResult, AdminVendorSnippet, AdminSlotProductSummary, AdminUpdateSlotCapacityResult, } from '@packages/shared' import { coerceDate } from '../lib/date' type SlotSnippetInput = { name: string skuIds: number[] validTill?: string } const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null return value.map((item) => String(item)) } const getNumberArray = (value: unknown): number[] => { if (!Array.isArray(value)) return [] return value.map((item) => Number(item)) } const normalizeSkuIds = (value: unknown): number[] => { if (!Array.isArray(value)) return [] const ids = value .map((item) => Number(item)) .filter((item) => Number.isFinite(item)) return Array.from(new Set(ids)) } const chunkArray = (items: T[], size: number): T[][] => { if (size <= 0) return [items] const chunks: T[][] = [] for (let i = 0; i < items.length; i += size) { chunks.push(items.slice(i, i + size)) } return chunks } const SKU_ID_CHUNK_SIZE = 40 const fetchExistingSkuIds = async (tx: any, skuIds: number[]) => { const existingIds = new Set() const chunks = chunkArray(skuIds, SKU_ID_CHUNK_SIZE) for (const chunk of chunks) { if (chunk.length === 0) continue const skus = await tx.query.productSkus.findMany({ where: inArray(productSkus.id, chunk), columns: { id: true }, }) skus.forEach((sku: { id: number }) => existingIds.add(sku.id)) } return existingIds } const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({ id: slot.id, deliveryTime: coerceDate(slot.deliveryTime) ?? new Date(0), freezeTime: coerceDate(slot.freezeTime) ?? new Date(0), isActive: slot.isActive, isFlash: slot.isFlash, isCapacityFull: slot.isCapacityFull, deliverySequence: slot.deliverySequence, groupIds: slot.groupIds, }) const mapSlotSkuSummary = (sku: { id: number; images: unknown; name: string | null; product: { name: string } | null; features: Array<{ featureName: string; featureValue: string }> }): AdminSlotProductSummary => ({ id: sku.id, name: sku.product?.name ?? 'Unknown', images: getStringArray(sku.images), skuName: sku.name ?? null, features: (sku.features || []).map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })), }) const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVendorSnippet => ({ id: snippet.id, snippetCode: snippet.snippetCode, slotId: snippet.slotId ?? null, skuIds: snippet.skuIds || [], isPermanent: snippet.isPermanent, validTill: coerceDate(snippet.validTill), createdAt: coerceDate(snippet.createdAt) ?? new Date(0), }) export async function getActiveSlotsWithProducts(limit: number = 20): Promise { const slots = await db.query.deliverySlotInfo .findMany({ where: eq(deliverySlotInfo.isActive, true), orderBy: desc(deliverySlotInfo.deliveryTime), limit, }) // Get all unique SKU IDs from all slots const allSkuIds = new Set() for (const slot of slots) { for (const skuId of (slot.skuIds || [])) { allSkuIds.add(skuId) } } // Fetch all SKUs in one query const skuIdsArray = Array.from(allSkuIds) const skuIdsSet = new Set(skuIdsArray) let skusData = await db.query.productSkus.findMany({ with: { features: true, product: true }, }) skusData = skusData.filter((item: any) => skuIdsSet.has(item.id)) // Create a map for quick lookup const skuMap = new Map(skusData.map((s: any) => [s.id, s])) return slots.map((slot) => ({ ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), products: (slot.skuIds || []) .map((skuId: number) => skuMap.get(skuId)) .filter((p): p is NonNullable => p != null) .map((sku: any) => mapSlotSkuSummary(sku)), })) } export async function staleSlotsCleanup(): Promise { // Get the 20 most recent slot IDs const recentSlots = await db .select({ id: deliverySlotInfo.id }) .from(deliverySlotInfo) .orderBy(desc(deliverySlotInfo.id)) .limit(20) if (recentSlots.length < 20) { return 0 // Less than 20 slots exist, nothing to clean } // Get the threshold (the 20th most recent slot ID - minimum of the 20) const threshold = Math.min(...recentSlots.map((s) => s.id)) // Clear productIds for all slots older than threshold const result = await db .update(deliverySlotInfo) .set({ skuIds: [] }) .where(eq(deliverySlotInfo.id, threshold)) return 1 } export async function getActiveSlots(): Promise { const slots = await db.query.deliverySlotInfo.findMany({ where: eq(deliverySlotInfo.isActive, true), }) return slots.map(mapDeliverySlot) } export async function getSlotsAfterDate(afterDate: Date): Promise { const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), gt(deliverySlotInfo.deliveryTime, afterDate) ), orderBy: asc(deliverySlotInfo.deliveryTime), }) return slots.map(mapDeliverySlot) } export async function getSlotByIdWithRelations(id: number): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, id), with: { vendorSnippets: true, }, }) if (!slot) { return null } // Fetch SKUs for this slot const skuIds = slot.skuIds || [] const skuIdSet = new Set(skuIds) let skusData = skuIds.length > 0 ? await db.query.productSkus.findMany({ with: { features: true, product: true }, columns: { id: true, images: true, name: true }, }) : [] skusData = skusData.filter((item: any) => skuIdSet.has(item.id)) return { ...mapDeliverySlot(slot), deliverySequence: getNumberArray(slot.deliverySequence), groupIds: getNumberArray(slot.groupIds), products: skusData.map((sku: any) => mapSlotSkuSummary(sku)), vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), } } export async function createSlotWithRelations(input: { deliveryTime: string freezeTime: string isActive?: boolean skuIds?: number[] vendorSnippets?: SlotSnippetInput[] groupIds?: number[] }): Promise { const { deliveryTime, freezeTime, isActive, skuIds, vendorSnippets: snippets, groupIds } = input const normalizedSkuIds = normalizeSkuIds(skuIds) const result = await db.transaction(async (tx) => { // Validate SKU IDs if provided if (normalizedSkuIds.length > 0) { const existingIds = await fetchExistingSkuIds(tx, normalizedSkuIds) const missingIds = normalizedSkuIds.filter((skuId) => !existingIds.has(skuId)) if (missingIds.length > 0) { throw new Error(`Invalid SKU IDs: ${missingIds.join(', ')}`) } } const [newSlot] = await tx .insert(deliverySlotInfo) .values({ deliveryTime: new Date(deliveryTime), freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: groupIds !== undefined ? groupIds : [], skuIds: normalizedSkuIds, }) .returning() let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { const skus = await tx.query.productSkus.findMany({ where: inArray(productSkus.id, snippet.skuIds), }) if (skus.length !== snippet.skuIds.length) { throw new Error(`One or more invalid SKU IDs in snippet "${snippet.name}"`) } const existingSnippet = await tx.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippet.name), }) if (existingSnippet) { throw new Error(`Snippet name "${snippet.name}" already exists`) } const [createdSnippet] = await tx.insert(vendorSnippets).values({ snippetCode: snippet.name, slotId: newSlot.id, skuIds: snippet.skuIds, validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, }).returning() createdSnippets.push(mapVendorSnippet(createdSnippet)) } } return { slot: mapDeliverySlot(newSlot), createdSnippets, message: 'Slot created successfully', } }) return result } export async function updateSlotWithRelations(input: { id: number deliveryTime: string freezeTime: string isActive?: boolean skuIds?: number[] vendorSnippets?: SlotSnippetInput[] groupIds?: number[] }): Promise { const { id, deliveryTime, freezeTime, isActive, skuIds, vendorSnippets: snippets, groupIds } = input let validGroupIds = groupIds if (groupIds && groupIds.length > 0) { const existingGroups = await db.query.productGroupInfo.findMany({ where: inArray(productGroupInfo.id, groupIds), columns: { id: true }, }) validGroupIds = existingGroups.map((group: { id: number }) => group.id) } const normalizedSkuIds = skuIds !== undefined ? normalizeSkuIds(skuIds) : undefined const result = await db.transaction(async (tx) => { // Validate SKU IDs if provided if (normalizedSkuIds !== undefined && normalizedSkuIds.length > 0) { const existingIds = await fetchExistingSkuIds(tx, normalizedSkuIds) const missingIds = normalizedSkuIds.filter((skuId) => !existingIds.has(skuId)) if (missingIds.length > 0) { throw new Error(`Invalid SKU IDs: ${missingIds.join(', ')}`) } } const [updatedSlot] = await tx .update(deliverySlotInfo) .set({ deliveryTime: new Date(deliveryTime), freezeTime: new Date(freezeTime), isActive: isActive !== undefined ? isActive : true, groupIds: validGroupIds !== undefined ? validGroupIds : [], ...(normalizedSkuIds !== undefined && { skuIds: normalizedSkuIds }), }) .where(eq(deliverySlotInfo.id, id)) .returning() if (!updatedSlot) { return null } let createdSnippets: AdminVendorSnippet[] = [] if (snippets && snippets.length > 0) { for (const snippet of snippets) { const skus = await tx.query.productSkus.findMany({ where: inArray(productSkus.id, snippet.skuIds), }) if (skus.length !== snippet.skuIds.length) { throw new Error(`One or more invalid SKU IDs in snippet "${snippet.name}"`) } const existingSnippet = await tx.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippet.name), }) if (existingSnippet) { throw new Error(`Snippet name "${snippet.name}" already exists`) } const [createdSnippet] = await tx.insert(vendorSnippets).values({ snippetCode: snippet.name, slotId: id, skuIds: snippet.skuIds, validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, }).returning() createdSnippets.push(mapVendorSnippet(createdSnippet)) } } return { slot: mapDeliverySlot(updatedSlot), createdSnippets, message: 'Slot updated successfully', } }) return result } export async function deleteSlotById(id: number): Promise { const [deletedSlot] = await db .update(deliverySlotInfo) .set({ isActive: false }) .where(eq(deliverySlotInfo.id, id)) .returning() if (!deletedSlot) { return null } return mapDeliverySlot(deletedSlot) } export async function getSlotDeliverySequence(slotId: number): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, slotId), }) if (!slot) { return null } return mapDeliverySlot(slot) } export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) { const [updatedSlot] = await db .update(deliverySlotInfo) .set({ deliverySequence: sequence as Record }) .where(eq(deliverySlotInfo.id, slotId)) .returning({ id: deliverySlotInfo.id, deliverySequence: deliverySlotInfo.deliverySequence, }) return updatedSlot || null } export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise { const [updatedSlot] = await db .update(deliverySlotInfo) .set({ isCapacityFull }) .where(eq(deliverySlotInfo.id, slotId)) .returning() if (!updatedSlot) { return null } return { success: true, slot: mapDeliverySlot(updatedSlot), message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, } }