import { describe, expect, it } from "vitest"; import { formatBp, holdingSummary, periodPoints, periodReport, runMovements, sumReports, type DatedMovement, type MovementInput, } from "./portfolio"; const buy = (quantity: number, amountMinor: number): MovementInput => ({ kind: "buy", quantity, amountMinor, }); describe("ağırlıklı ortalama maliyet", () => { it("örnek senaryo: 10 gr 25.000 TL'ye, her ay ekleme", () => { // 10 gr × 2.500, 2 gr × 2.600, 2 gr × 2.700 → 14 gr, 35.600 TL; ort. 2.542,857… const run = runMovements([buy(10_000, 25_000_00), buy(2_000, 5_200_00), buy(2_000, 5_400_00)]); expect(run.quantity).toBe(14_000); expect(run.costMinor).toBe(35_600_00); expect(run.investedMinor).toBe(35_600_00); const s = holdingSummary(run, 3_000_00); expect(s.avgCostMinor).toBe(2_542_86); expect(s.currentValueMinor).toBe(42_000_00); expect(s.unrealizedMinor).toBe(6_400_00); // 6.400 / 35.600 = %17,98 expect(s.unrealizedBp).toBe(1798); }); it("satış: ortalama maliyetle düşer, gerçekleşmiş kâr/zarar", () => { const run = runMovements([ buy(10_000, 30_000_00), buy(10_000, 40_000_00), { kind: "sell", quantity: 5_000, amountMinor: 20_000_00 }, ]); // ort. 3.500 → 5 gr maliyet 17.500; satış 20.000 → +2.500 expect(run.steps[2]).toMatchObject({ costOutMinor: 17_500_00, realizedMinor: 2_500_00 }); expect(run.quantity).toBe(15_000); expect(run.costMinor).toBe(52_500_00); expect(run.proceedsMinor).toBe(20_000_00); // Kalan ortalama değişmez. expect(holdingSummary(run, null).avgCostMinor).toBe(3_500_00); }); it("hediye çıkışı K/Z yazmaz; gelen hediye yatırım sayılmaz; tamamı çıkınca maliyet sıfır", () => { const run = runMovements([ buy(3_000, 9_000_00), { kind: "gift_in", quantity: 1_000, amountMinor: 3_300_00 }, { kind: "gift_out", quantity: 2_000, amountMinor: 0 }, { kind: "sell", quantity: 2_000, amountMinor: 7_000_00 }, ]); expect(run.investedMinor).toBe(9_000_00); expect(run.giftInMinor).toBe(3_300_00); // ort. 3.075 → çıkış 6.150, kalan 6.150; satış tümü → K/Z 850 expect(run.giftOutCostMinor).toBe(6_150_00); expect(run.realizedMinor).toBe(850_00); expect(run.quantity).toBe(0); expect(run.costMinor).toBe(0); const s = holdingSummary(run, 3_500_00); expect(s).toMatchObject({ avgCostMinor: null, currentValueMinor: 0, unrealizedBp: null }); }); it("eldekinden fazla çıkış işaretlenir", () => { const run = runMovements([ buy(1_000, 100_00), { kind: "sell", quantity: 2_000, amountMinor: 1 }, ]); expect(run.negativeAt).toBe(1); expect(runMovements([buy(1_000, 100_00)]).negativeAt).toBeNull(); }); it("oran biçimi", () => { expect(formatBp(1798)).toBe("+%17,98"); expect(formatBp(-350)).toBe("−%3,5"); expect(formatBp(0)).toBe("%0"); }); }); describe("dönemsel rapor", () => { it("noktalar: başlangıç, ay sonları, bitiş", () => { expect(periodPoints("2026-05-06", "2026-08-15")).toEqual([ "2026-05-06", "2026-05-31", "2026-06-30", "2026-07-31", "2026-08-15", ]); expect(periodPoints("2026-05-31", "2026-06-30")).toEqual(["2026-05-31", "2026-06-30"]); }); // Örnek: 10 gr'ı 25.000'e aldım, her ay 1 gr ekledim; 6 ay sonra ne kazandım? const prices: Record = { "2026-01-01": 2_500_00, "2026-02-01": 2_600_00, "2026-03-01": 2_700_00, "2026-07-01": 3_000_00, }; const priceAt = (date: string) => { const keys = Object.keys(prices) .filter((k) => k <= date) .sort(); return keys.length ? prices[keys.at(-1)!]! : null; }; const ms: DatedMovement[] = [ { kind: "buy", date: "2026-01-01", quantity: 10_000, amountMinor: 25_000_00 }, { kind: "buy", date: "2026-02-01", quantity: 1_000, amountMinor: 2_600_00 }, { kind: "buy", date: "2026-03-01", quantity: 1_000, amountMinor: 2_700_00 }, { kind: "gift_out", date: "2026-07-01", quantity: 1_000, amountMinor: 0 }, ]; it("örnek senaryo: yatırılan, dönem sonu değer, kâr", () => { const r = periodReport(ms, priceAt, "2026-01-01", "2026-07-02"); expect(r.startValueMinor).toBe(0); expect(r.investedMinor).toBe(30_300_00); // 11 gr × 3.000 = 33.000; çıkan 1 gr o günkü değeri 3.000 expect(r.endValueMinor).toBe(33_000_00); expect(r.giftOutMinor).toBe(3_000_00); expect(r.profitMinor).toBe(5_700_00); expect(r.profitBp).toBe(1881); expect(r.points.map((p) => p.valueMinor)).toEqual([ 0, 25_000_00, 28_600_00, 32_400_00, 32_400_00, 32_400_00, 32_400_00, 33_000_00, ]); }); it("başlangıçta eldeki değer dönem içi kârdan düşülür; fiyat yoksa null", () => { const r = periodReport(ms, priceAt, "2026-03-15", "2026-07-02"); expect(r.startValueMinor).toBe(32_400_00); expect(r.investedMinor).toBe(0); expect(r.profitMinor).toBe(33_000_00 + 3_000_00 - 32_400_00); expect(periodReport(ms, () => null, "2026-01-01", "2026-07-02").profitMinor).toBeNull(); }); it("toplam", () => { const a = periodReport(ms, priceAt, "2026-01-01", "2026-07-02"); const t = sumReports([a, a], "2026-01-01", "2026-07-02"); expect(t.investedMinor).toBe(60_600_00); expect(t.profitMinor).toBe(11_400_00); expect(t.points.at(-1)!.valueMinor).toBe(66_000_00); expect(sumReports([], "2026-01-01", "2026-01-31").points).toHaveLength(2); }); });