freshyo/packages/db_helper_sqlite/src/admin-apis/slots.ts
2026-07-29 22:42:40 +05:30

427 lines
13 KiB
TypeScript

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 = <T>(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<number>()
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<AdminSlotWithProducts[]> {
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<number>()
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<typeof p> => p != null)
.map((sku: any) => mapSlotSkuSummary(sku)),
}))
}
export async function staleSlotsCleanup(): Promise<number> {
// 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<AdminDeliverySlot[]> {
const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
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<AdminSlotWithProductsAndSnippetsBase | null> {
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<AdminSlotCreateResult> {
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<AdminSlotUpdateResult | null> {
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<AdminDeliverySlot | null> {
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<AdminDeliverySlot | null> {
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<string, number> })
.where(eq(deliverySlotInfo.id, slotId))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
})
return updatedSlot || null
}
export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise<AdminUpdateSlotCapacityResult | null> {
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'}`,
}
}