feat(priser): kvittopris -> lager + blandat marknadspris per ingrediens
Bekraftade kvittovaror far sitt netto-pris (efter rabatt) i lagret. Prisupp- skattningar anvander nu ett robust marknadssnitt: median av observerade pris/kg fran lagret, klampat mot schablonen och viktat efter antal observationer -> ett 'realtidsnara' pris dar en extremt dyr/billig butik inte drar ivag snittet.
This commit is contained in:
@@ -74,19 +74,42 @@ export async function processScanJob(ctx: WorkerContext, scanJobId: string): Pro
|
||||
// till items och kör samma kanoniska namnmatchning som för foton.
|
||||
if (job.jobType === "READ_RECEIPT" && Array.isArray((output as { lines?: unknown }).lines)) {
|
||||
const lines = (output as { lines: Array<Record<string, unknown>> }).lines;
|
||||
// Nettopris per vara: dra av matchande rabattrader (matchas på normalizedName)
|
||||
// så lagret får det pris användaren faktiskt betalade, inte listpriset.
|
||||
const discountByName = new Map<string, number>();
|
||||
for (const l of lines) {
|
||||
if (l.isDiscount === true && l.normalizedName && typeof l.totalPriceMinor === "number") {
|
||||
const k = String(l.normalizedName).toLowerCase();
|
||||
discountByName.set(k, (discountByName.get(k) ?? 0) + l.totalPriceMinor); // negativt belopp
|
||||
}
|
||||
}
|
||||
(output as Record<string, unknown>).items = lines
|
||||
.filter((l) => l.isDiscount !== true && (l.normalizedName ?? l.rawText))
|
||||
.map((l, idx) => ({
|
||||
tempId: String(idx),
|
||||
detectedName: String(l.normalizedName ?? l.rawText ?? ""),
|
||||
canonicalIngredientId: (l.canonicalIngredientId as string | null) ?? null,
|
||||
brand: null,
|
||||
estimatedQuantity: typeof l.quantity === "number" ? l.quantity : null,
|
||||
unit: (l.unit as string | null) ?? null,
|
||||
bestBeforeDate: null,
|
||||
confidence: typeof l.confidence === "number" ? l.confidence : 0.5,
|
||||
requiresConfirmation: true,
|
||||
}));
|
||||
.map((l, idx) => {
|
||||
const qty = typeof l.quantity === "number" ? l.quantity : null;
|
||||
const gross =
|
||||
typeof l.totalPriceMinor === "number"
|
||||
? l.totalPriceMinor
|
||||
: typeof l.unitPriceMinor === "number" && qty != null
|
||||
? Math.round(l.unitPriceMinor * qty)
|
||||
: null;
|
||||
const disc = l.normalizedName
|
||||
? (discountByName.get(String(l.normalizedName).toLowerCase()) ?? 0)
|
||||
: 0;
|
||||
const priceMinor = gross != null ? Math.max(0, gross + disc) : null;
|
||||
return {
|
||||
tempId: String(idx),
|
||||
detectedName: String(l.normalizedName ?? l.rawText ?? ""),
|
||||
canonicalIngredientId: (l.canonicalIngredientId as string | null) ?? null,
|
||||
brand: null,
|
||||
estimatedQuantity: qty,
|
||||
unit: (l.unit as string | null) ?? null,
|
||||
bestBeforeDate: null,
|
||||
confidence: typeof l.confidence === "number" ? l.confidence : 0.5,
|
||||
requiresConfirmation: true,
|
||||
priceMinor, // kvittots (netto)pris i ören, förs vidare till lagret vid bekräftelse
|
||||
};
|
||||
});
|
||||
output = await mapDetectedItemsToCanonical(ctx, output);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user