import { NextRequest, NextResponse } from "next/server"; import { db, pantryItems, eq, asc, inArray } from "@epicure/db"; import { requireSessionOrApiKey } from "@/lib/api-auth"; import { loadIngredientAliasIndex, resolveIngredientKey } from "@/lib/ingredient-match"; function normalizeUnit(unit: string | null): string { return (unit ?? "").trim().toLowerCase(); } /** * One-shot cleanup for pantry items that turn out to be the same ingredient * under different names ("sel", "sel fin", "sel de table") — a case the * alias index (lib/ingredient-match.ts) only prevents going forward, not * for rows added before it existed. Groups by resolved ingredient key alone * (unit is NOT part of the grouping key — two rows of the same ingredient * still merge even if one has no quantity/unit at all, or the two use * different units); for any group with more than one row, merges into the * oldest row and deletes the rest. * * Quantities are only summed when every row in the group has a parseable * quantity AND they all share the same unit — mixing units without * conversion, or mixing a known and an unknown amount, would silently * invent a number, so the first known (quantity, unit) pair is kept * instead. Notes are concatenated (nothing is dropped); expiresAt keeps the * soonest date (the conservative choice — better to under-promise * freshness than over). */ export async function POST(req: NextRequest) { const { session, response } = await requireSessionOrApiKey(req); if (response) return response; const userId = session!.user.id; const [items, aliasIndex] = await Promise.all([ db.query.pantryItems.findMany({ where: eq(pantryItems.userId, userId), orderBy: asc(pantryItems.createdAt) }), loadIngredientAliasIndex(), ]); const groups = new Map(); for (const item of items) { const key = resolveIngredientKey(item.rawName, aliasIndex); const group = groups.get(key) ?? []; group.push(item); groups.set(key, group); } let mergedGroups = 0; let removed = 0; const idsToDelete: string[] = []; for (const group of groups.values()) { if (group.length < 2) continue; mergedGroups++; const [survivor, ...rest] = group; const quantities = group.map((i) => (i.quantity ? parseFloat(i.quantity) : null)); const units = group.map((i) => normalizeUnit(i.unit)); const sameUnit = units.every((u) => u === units[0]); const allParseable = sameUnit && quantities.every((q) => q !== null && !isNaN(q)); const firstKnownIndex = quantities.findIndex((q) => q !== null && !isNaN(q)); const mergedQuantity = allParseable ? String(quantities.reduce((sum, q) => sum! + q!, 0)) : firstKnownIndex !== -1 ? quantities[firstKnownIndex]!.toString() : survivor!.quantity; const mergedUnit = allParseable ? survivor!.unit : firstKnownIndex !== -1 ? group[firstKnownIndex]!.unit : survivor!.unit; const mergedNotes = [...new Set(group.map((i) => i.notes?.trim()).filter((n): n is string => !!n))].join("; ") || null; const mergedAisle = group.find((i) => i.aisle)?.aisle ?? null; const mergedIngredientId = group.find((i) => i.ingredientId)?.ingredientId ?? null; const expiryDates = group.map((i) => i.expiresAt).filter((d): d is Date => d !== null); const mergedExpiresAt = expiryDates.length > 0 ? new Date(Math.min(...expiryDates.map((d) => d.getTime()))) : null; await db.update(pantryItems).set({ quantity: mergedQuantity, unit: mergedUnit, notes: mergedNotes, aisle: mergedAisle, ingredientId: mergedIngredientId, expiresAt: mergedExpiresAt, }).where(eq(pantryItems.id, survivor!.id)); idsToDelete.push(...rest.map((i) => i.id)); removed += rest.length; } if (idsToDelete.length > 0) { await db.delete(pantryItems).where(inArray(pantryItems.id, idsToDelete)); } return NextResponse.json({ mergedGroups, removed }); }