65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
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<InventoryTransactionType> = new Set([
|
||
"consume",
|
||
"discard",
|
||
"cook_use",
|
||
"leftover_consumed",
|
||
]);
|
||
/** Transaktionstyper som ska vara positiva (inflöde). */
|
||
const INFLOW_TYPES: ReadonlySet<InventoryTransactionType> = 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;
|
||
}
|