Files
butcem/src/shared/shopping-compare.ts
T
suphicakir bd5a20ce64 feat(shopping): alışverişi tamamla transaction'ı ve karşılaştırma motoru
- tek transaction: stok alımı, fiş kaynaklı fiyat, mağaza başına bölünmüş gider, fiş ekleri, kalanların taşınması
- muadil, markasız ürün tipi ve plansız ekstra satırları; stok → gider kategorisi eşlemesi ve yedek kategori
- compareShopping saf fonksiyonu: aynı/muadil/alınmadı/ekstra, farkın nedenlere tam ayrışması
- tamamlanan listede rapor görüntüsü; testler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:57:50 +03:00

116 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Alışveriş sonrası karşılaştırma (Bölüm 6.12). Saf fonksiyonlar.
*
* Her liste kalemi için: aynı (planlanan ürün alındı), muadil (aynı ürün tipinde başka marka/paket),
* alınmadı; listede olmayanlar plansız ekstra. Planlanan tutar, tamamlama anındaki akıllı önerinin
* (önerilen dağılım) kalem maliyetidir; fiyatı bilinmeyen kalemin planı yoktur.
*
* Fark (gerçek − plan) nedenlere ayrıştırılır ve toplamları farka tam eşittir:
* - fiyat: aynı ürünün paket fiyatı farkı × alınan paket,
* - miktar: (alınan − planlanan paket) × planlanan paket fiyatı,
* - muadil: muadil kalemlerde gerçek − plan,
* - alınmayan: alınmayan planlı kalemlerin planı (eksi),
* - ekstra: plansız ekstraların tutarı,
* - fiyatsız: planı olmayan (fiyatı bilinmeyen) kalemlerin tutarı.
*/
export const MATCH_KINDS = ["same", "substitute", "skipped", "extra"] as const;
export type MatchKind = (typeof MATCH_KINDS)[number];
/** Liste kalemi veya ekstra için gerçekleşen alım. Paket adedi binde bir. */
export type ActualPurchase = { productId: string | null; packages: number; amountMinor: number };
/** Akıllı önerinin kalem için planı. */
export type PlannedPurchase = {
productId: string;
packages: number;
priceMinor: number;
costMinor: number;
};
/**
* Kalemin eşleşmesi. Ürün kalemi: aynı ürün → aynı. Ürün tipi kalemi: planlanan markayla aynı
* ürün (ya da plan yoksa herhangi bir ürün) → aynı; başka marka/paket → muadil. Serbest metin
* kalemi alındıysa aynı.
*/
export function matchKind(
item: { kind: "type" | "product" | "text"; productId: string | null } | null,
plan: PlannedPurchase | null,
actual: ActualPurchase | null,
): MatchKind {
if (!item) return "extra";
if (!actual) return "skipped";
if (item.kind === "text") return "same";
const expected = item.kind === "product" ? item.productId : (plan?.productId ?? null);
return expected === null || expected === actual.productId ? "same" : "substitute";
}
export type ComparisonRow = {
match: MatchKind;
plan: PlannedPurchase | null;
actual: ActualPurchase | null;
};
export type ComparisonReasons = {
price: number;
quantity: number;
substitute: number;
skipped: number;
extra: number;
unplanned: number;
};
export type ComparisonSummary = {
plannedMinor: number;
actualMinor: number;
diffMinor: number;
reasons: ComparisonReasons;
counts: Record<MatchKind, number>;
};
export function compareShopping(rows: readonly ComparisonRow[]): ComparisonSummary {
const reasons: ComparisonReasons = {
price: 0,
quantity: 0,
substitute: 0,
skipped: 0,
extra: 0,
unplanned: 0,
};
const counts: Record<MatchKind, number> = { same: 0, substitute: 0, skipped: 0, extra: 0 };
let plannedMinor = 0;
let actualMinor = 0;
for (const r of rows) {
counts[r.match]++;
if (r.plan) plannedMinor += r.plan.costMinor;
if (r.actual) actualMinor += r.actual.amountMinor;
switch (r.match) {
case "extra":
reasons.extra += r.actual?.amountMinor ?? 0;
break;
case "skipped":
reasons.skipped -= r.plan?.costMinor ?? 0;
break;
case "substitute":
if (r.plan) reasons.substitute += r.actual!.amountMinor - r.plan.costMinor;
else reasons.unplanned += r.actual!.amountMinor;
break;
case "same": {
const a = r.actual!;
if (!r.plan || a.productId !== r.plan.productId) {
// Plansız (fiyatı bilinmeyen) ya da serbest metin.
if (r.plan) reasons.substitute += a.amountMinor - r.plan.costMinor;
else reasons.unplanned += a.amountMinor;
break;
}
// Paket adedi binde bir: plan fiyatıyla beklenen = paket × fiyat.
const expectedAtPlanPrice = Math.round((a.packages * r.plan.priceMinor) / 1000);
reasons.price += a.amountMinor - expectedAtPlanPrice;
reasons.quantity += expectedAtPlanPrice - r.plan.costMinor;
break;
}
}
}
return { plannedMinor, actualMinor, diffMinor: actualMinor - plannedMinor, reasons, counts };
}