fix(inventory): robustare dedup vid skann + borttag pa startsidan

Dedup vid /v1/scans/:id/confirm matchade bara pa exakt (skiftlageskansligt)
namn eller identiskt katalog-id, sa omfotografering av samma hylla skapade
dubbletter. Ny normalizeItemName (gemener, accenter, storlek bort; behaller
fetthalt-siffror) + matchning pa katalog-id ELLER normaliserat namn mot
aktivt hushallslager.

Mobil: 'Ta bort' fanns bara i Ditt kok. Lade borttag pa startsidans
lager-rader (befintlig DELETE /v1/inventory/items/:id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WZdew6cWn1MeHqoYWFfxzo
This commit is contained in:
Claude
2026-08-18 14:44:03 +00:00
parent 4ca567bba3
commit 684743ae95
4 changed files with 86 additions and 23 deletions
+29 -19
View File
@@ -7,6 +7,7 @@ import { confirmScanInputSchema, createScanInputSchema, idParamSchema } from "@a
import { errors, parse } from "../lib/errors.js";
import { emitEvent, requireActiveHousehold } from "../lib/helpers.js";
import { consumeAiScan } from "../lib/entitlements.js";
import { normalizeItemName } from "@app/inventory-engine";
/**
* Skanningsflödet (spec §50):
@@ -141,6 +142,23 @@ export async function scanRoutes(app: FastifyInstance) {
const created: string[] = [];
const proposals = extractProposals(job);
// Aktiva varor i hushållet, för dubblett-hopslagning vid bekräftelse.
const activeRows = await app.db
.select({
id: schema.inventoryItems.id,
canonicalIngredientId: schema.inventoryItems.canonicalIngredientId,
displayName: schema.inventoryItems.displayName,
})
.from(schema.inventoryItems)
.where(
and(
eq(schema.inventoryItems.householdId, householdId),
gt(schema.inventoryItems.quantity, 0),
),
);
const activeItems = activeRows.map((r) => ({ ...r, norm: normalizeItemName(r.displayName) }));
for (const item of input.items) {
const proposal = findProposal(proposals, item.tempId);
await recordCorrection(app, job, item, proposal);
@@ -150,25 +168,17 @@ export async function scanRoutes(app: FastifyInstance) {
if (!locationId)
throw errors.badRequest("storageLocationId saknas och ingen standardplats finns.");
// Dedup (#1): finns varan redan aktiv i hushållet? Uppdatera + hoppa över,
// överlappande foton / omfotografering inte skapar dubletter.
const dedupCond = item.canonicalIngredientId
? and(
eq(schema.inventoryItems.householdId, householdId),
eq(schema.inventoryItems.canonicalIngredientId, item.canonicalIngredientId),
gt(schema.inventoryItems.quantity, 0),
)
: and(
eq(schema.inventoryItems.householdId, householdId),
isNull(schema.inventoryItems.canonicalIngredientId),
eq(schema.inventoryItems.displayName, item.displayName),
gt(schema.inventoryItems.quantity, 0),
);
const [dupe] = await app.db
.select({ id: schema.inventoryItems.id })
.from(schema.inventoryItems)
.where(dedupCond)
.limit(1);
// Dedup (#1): slå ihop om varan redan finns aktiv i hushållet, så att
// överlappande foton / omfotografering av samma hylla inte dubbellagras.
// Matcha på katalog-id ELLER normaliserat namn — Vision skriver sällan
// exakt samma namn två gånger ("Mjölk" vs "mjölk" vs "Mjölk 1L").
const wantNorm = normalizeItemName(item.displayName);
const dupe = activeItems.find(
(r) =>
(item.canonicalIngredientId != null &&
r.canonicalIngredientId === item.canonicalIngredientId) ||
r.norm === wantNorm,
);
if (dupe) {
await app.db
.update(schema.inventoryItems)