import type { InventoryTransactionType, Unit } from "@app/shared-types"; export interface TransactionLike { type: InventoryTransactionType; quantityDelta: number; unit: Unit; } /** Transaktionstyper som ska vara negativa (uttag). */ const OUTFLOW_TYPES: ReadonlySet = new Set([ "consume", "discard", "cook_use", "leftover_consumed", ]); /** Transaktionstyper som ska vara positiva (inflöde). */ const INFLOW_TYPES: ReadonlySet = new Set([ "purchase", "leftover_created", ]); export interface BalanceResult { balance: number; valid: boolean; errors: string[]; } /** * Beräkna saldo ur transaktionshistorik (spec §8: transaktioner är sanningen). * Saldo tillåts aldrig bli negativt – i så fall flaggas historiken som * inkonsistent så att en correction kan föreslås, i stället för tyst clamp. */ export function computeBalance(transactions: TransactionLike[]): BalanceResult { let balance = 0; const errors: string[] = []; for (const [i, tx] of transactions.entries()) { if (OUTFLOW_TYPES.has(tx.type) && tx.quantityDelta > 0) { errors.push(`Transaktion ${i} (${tx.type}) borde vara negativ men är +${tx.quantityDelta}`); } if (INFLOW_TYPES.has(tx.type) && tx.quantityDelta < 0) { errors.push(`Transaktion ${i} (${tx.type}) borde vara positiv men är ${tx.quantityDelta}`); } balance += tx.quantityDelta; if (balance < -1e-9) { errors.push( `Saldo blev negativt (${balance.toFixed(3)}) efter transaktion ${i} (${tx.type})`, ); balance = 0; } } return { balance: round3(balance), valid: errors.length === 0, errors }; } /** Normalisera ett uttag: rätt tecken oavsett hur anroparen skickade mängden. */ export function normalizeDelta(type: InventoryTransactionType, quantity: number): number { const magnitude = Math.abs(quantity); if (OUTFLOW_TYPES.has(type)) return -magnitude; if (INFLOW_TYPES.has(type)) return magnitude; return quantity; // adjust/correction/move/freeze/thaw får vara valfritt tecken } function round3(v: number): number { return Math.round(v * 1000) / 1000; }