feat(reports): cari, birikim, alışveriş, birim fiyat ve fire rapor servisleri
- mağaza adı eşleşmesi ve aylık birim fiyat özeti (saf, birim testli) - tamamlanan listelerin plan/gerçekleşen özeti, mağaza bazlı harcama - ürün tipi fiyat geçmişi yükleyicisi; fire oranı ve tahmini maliyet; testler Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { categoryBreakdown, changePct, savingsRate, sharePct } from "./reports";
|
||||
import {
|
||||
categoryBreakdown,
|
||||
changePct,
|
||||
matchStoreName,
|
||||
monthlyUnitPrices,
|
||||
savingsRate,
|
||||
sharePct,
|
||||
} from "./reports";
|
||||
|
||||
describe("oranlar", () => {
|
||||
it("toplam gidere oran bir ondalıklı (35.000 içinde 5.000 → %14,3)", () => {
|
||||
@@ -60,3 +67,34 @@ describe("categoryBreakdown", () => {
|
||||
expect(b.rows.find((r) => r.id === "ulasim")!.changeMinor).toBe(-1_000_00);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchStoreName", () => {
|
||||
const fold = (s: string) => s.toLocaleLowerCase("tr-TR");
|
||||
const names = ["Şok", "Migros", "Migros Jet", "A101"];
|
||||
it("katlanmış başlangıç eşleşmesi, en uzun ad, kelime sınırı", () => {
|
||||
expect(matchStoreName("Şok · Rasimpaşa", names, fold)).toBe("Şok");
|
||||
expect(matchStoreName("ŞOK MARKET", names, fold)).toBe("Şok");
|
||||
expect(matchStoreName("Migros Jet Moda", names, fold)).toBe("Migros Jet");
|
||||
expect(matchStoreName("A101x", names, fold)).toBeNull();
|
||||
expect(matchStoreName("Ev sahibi", names, fold)).toBeNull();
|
||||
expect(matchStoreName("", names, fold)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("monthlyUnitPrices", () => {
|
||||
it("ay başına en düşük ve ortalama; kayıt yoksa boş", () => {
|
||||
const out = monthlyUnitPrices(
|
||||
[
|
||||
{ month: "2026-10", unitPriceMinor: 100 },
|
||||
{ month: "2026-10", unitPriceMinor: 140 },
|
||||
{ month: "2026-11", unitPriceMinor: 90 },
|
||||
],
|
||||
["2026-09", "2026-10", "2026-11"],
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{ month: "2026-09", minMinor: null, avgMinor: null, count: 0 },
|
||||
{ month: "2026-10", minMinor: 100, avgMinor: 120, count: 2 },
|
||||
{ month: "2026-11", minMinor: 90, avgMinor: 90, count: 1 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,3 +118,58 @@ export function categoryBreakdown(
|
||||
rows.sort((a, b) => b.amountMinor - a.amountMinor || a.name.localeCompare(b.name, "tr"));
|
||||
return { totalMinor, prevTotalMinor, rows };
|
||||
}
|
||||
|
||||
// --- Alışveriş raporları ---
|
||||
|
||||
/** Rapor dönemi: içinde bulunulan dahil son N bütçe ayı. */
|
||||
export const REPORT_PERIODS = [3, 6, 12] as const;
|
||||
export type ReportPeriod = (typeof REPORT_PERIODS)[number];
|
||||
|
||||
export const parsePeriod = (x: unknown): ReportPeriod =>
|
||||
REPORT_PERIODS.includes(Number(x) as ReportPeriod) ? (Number(x) as ReportPeriod) : 6;
|
||||
|
||||
/**
|
||||
* Gider alıcısını mağaza adına eşler: Türkçe katlanmış alıcı adı mağaza adıyla başlıyorsa
|
||||
* (en uzun eşleşme). "Şok · Rasimpaşa" ve "ŞOK MARKET" → "Şok". Eşleşme yoksa null.
|
||||
*/
|
||||
export function matchStoreName(
|
||||
payee: string,
|
||||
storeNames: readonly string[],
|
||||
fold: (s: string) => string,
|
||||
): string | null {
|
||||
const p = fold(payee).trim();
|
||||
if (!p) return null;
|
||||
let best: string | null = null;
|
||||
for (const name of storeNames) {
|
||||
const n = fold(name).trim();
|
||||
if (!n || !p.startsWith(n)) continue;
|
||||
// Kelime sınırı: "a101x" "a101" ile eşleşmesin.
|
||||
const next = p.charAt(n.length);
|
||||
if (next && /[\p{L}\p{N}]/u.test(next)) continue;
|
||||
if (!best || n.length > fold(best).length) best = name;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export type UnitPricePoint = {
|
||||
month: string;
|
||||
minMinor: number | null;
|
||||
avgMinor: number | null;
|
||||
count: number;
|
||||
};
|
||||
|
||||
/** Ay başına en düşük ve ortalama birim fiyat (kayıt yoksa boş). Birim fiyat kesirli kuruş. */
|
||||
export function monthlyUnitPrices(
|
||||
records: readonly { month: string; unitPriceMinor: number }[],
|
||||
months: readonly string[],
|
||||
): UnitPricePoint[] {
|
||||
return months.map((month) => {
|
||||
const xs = records.filter((r) => r.month === month).map((r) => r.unitPriceMinor);
|
||||
return {
|
||||
month,
|
||||
minMinor: xs.length ? Math.min(...xs) : null,
|
||||
avgMinor: xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null,
|
||||
count: xs.length,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user