feat(shared): işlem şeması, bakiye etkisi, etiket ve CSV yardımcıları
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toCsv, csvMoney } from "./csv";
|
||||
import { transactionFormSchema } from "./schemas/transaction";
|
||||
import { normalizeTags, transactionEffects } from "./transactions";
|
||||
|
||||
const A = "a".repeat(24);
|
||||
const B = "b".repeat(24);
|
||||
const C1 = "c".repeat(24);
|
||||
const C2 = "d".repeat(24);
|
||||
|
||||
describe("transactionEffects", () => {
|
||||
it("gelir +, gider −, transfer kaynaktan − hedefe +", () => {
|
||||
expect(
|
||||
transactionEffects({ type: "income", status: "posted", amountMinor: 500, accountId: A }),
|
||||
).toEqual([{ accountId: A, deltaMinor: 500 }]);
|
||||
expect(
|
||||
transactionEffects({ type: "expense", status: "posted", amountMinor: 500, accountId: A }),
|
||||
).toEqual([{ accountId: A, deltaMinor: -500 }]);
|
||||
expect(
|
||||
transactionEffects({
|
||||
type: "transfer",
|
||||
status: "posted",
|
||||
amountMinor: 500,
|
||||
accountId: A,
|
||||
toAccountId: B,
|
||||
}),
|
||||
).toEqual([
|
||||
{ accountId: A, deltaMinor: -500 },
|
||||
{ accountId: B, deltaMinor: 500 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("planlı, iptal ve silinmiş işlemler etkisizdir", () => {
|
||||
for (const x of [
|
||||
{ status: "planned" as const },
|
||||
{ status: "void" as const },
|
||||
{ status: "posted" as const, deleted: true },
|
||||
]) {
|
||||
expect(transactionEffects({ type: "expense", amountMinor: 1, accountId: A, ...x })).toEqual(
|
||||
[],
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeTags", () => {
|
||||
it("küçük harf, kırpma, tekrar yok, en fazla 10", () => {
|
||||
expect(normalizeTags(" Market, İNDİRİM ,market,,#tatil")).toEqual([
|
||||
"market",
|
||||
"indirim",
|
||||
"tatil",
|
||||
]);
|
||||
expect(normalizeTags(Array.from({ length: 15 }, (_, i) => `e${i}`))).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transactionFormSchema", () => {
|
||||
const base = { type: "expense", amount: "1.200,00", date: "2026-11-05", accountId: A };
|
||||
|
||||
it("tek kategorili gideri kabul eder ve tutarı kuruşa çevirir", () => {
|
||||
const r = transactionFormSchema.parse({ ...base, categoryId: C1, tags: "ev, Ev" });
|
||||
expect(r.amount).toBe(120000);
|
||||
expect(r.categoryId).toBe(C1);
|
||||
expect(r.tags).toEqual(["ev"]);
|
||||
expect(r.splits).toEqual([]);
|
||||
});
|
||||
|
||||
it("kategorisiz ve bölmesiz gideri reddeder", () => {
|
||||
expect(transactionFormSchema.safeParse(base).success).toBe(false);
|
||||
});
|
||||
|
||||
it("bölünmüş işlemde kalem toplamı tutara eşit olmalı", () => {
|
||||
const ok = transactionFormSchema.safeParse({
|
||||
...base,
|
||||
splits: [
|
||||
{ categoryId: C1, amount: "800" },
|
||||
{ categoryId: C2, amount: "250" },
|
||||
{ categoryId: C1, amount: "150,00" },
|
||||
],
|
||||
});
|
||||
expect(ok.success).toBe(true);
|
||||
const bad = transactionFormSchema.safeParse({
|
||||
...base,
|
||||
splits: [
|
||||
{ categoryId: C1, amount: "800" },
|
||||
{ categoryId: C2, amount: "250" },
|
||||
],
|
||||
});
|
||||
expect(bad.success).toBe(false);
|
||||
expect(bad.error?.issues[0]?.path).toEqual(["splits"]);
|
||||
const single = transactionFormSchema.safeParse({
|
||||
...base,
|
||||
splits: [{ categoryId: C1, amount: "1200" }],
|
||||
});
|
||||
expect(single.success).toBe(false);
|
||||
});
|
||||
|
||||
it("transfer: hedef zorunlu, kaynaktan farklı", () => {
|
||||
expect(transactionFormSchema.safeParse({ ...base, type: "transfer" }).success).toBe(false);
|
||||
expect(
|
||||
transactionFormSchema.safeParse({ ...base, type: "transfer", toAccountId: A }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
transactionFormSchema.safeParse({ ...base, type: "transfer", toAccountId: B }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("sıfır, negatif ve geçersiz tutarı reddeder", () => {
|
||||
for (const amount of ["0", "-5", "abc", ""]) {
|
||||
expect(
|
||||
transactionFormSchema.safeParse({ ...base, categoryId: C1, amount }).success,
|
||||
amount,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSV", () => {
|
||||
it("BOM, noktalı virgül, kaçış ve formül enjeksiyonu koruması", () => {
|
||||
const csv = toCsv(
|
||||
["A", "B"],
|
||||
[
|
||||
["x;y", '"tırnak"'],
|
||||
["=HYPERLINK(1)", csvMoney(-125050)],
|
||||
],
|
||||
);
|
||||
expect(csv.startsWith("")).toBe(true);
|
||||
expect(csv).toContain('"x;y";"""tırnak"""');
|
||||
expect(csv).toContain("'=HYPERLINK(1);-1250,50");
|
||||
expect(csv.endsWith("\r\n")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user