baec70d324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
271 lines
9.1 KiB
TypeScript
271 lines
9.1 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||
import { listAccounts } from "@/features/accounts/service";
|
||
import {
|
||
getTransaction,
|
||
listTransactions,
|
||
updateTransaction,
|
||
} from "@/features/transactions/service";
|
||
import { currentMonth, getAccountStatement } from "@/features/transactions/statement";
|
||
import { AccountModel } from "@/models/account";
|
||
import { todayIstanbul } from "@/shared/dates";
|
||
import { addDays } from "@/shared/recurrence";
|
||
import {
|
||
contactEntrySchema,
|
||
contactFormSchema,
|
||
type ContactEntryInput,
|
||
type ContactFormInput,
|
||
} from "@/shared/schemas/contact";
|
||
import { transactionFilterSchema } from "@/shared/schemas/transaction";
|
||
import { setupSeededDb, teardownDb } from "@/test/seeded-db";
|
||
import {
|
||
countDueContacts,
|
||
createContact,
|
||
createContactEntry,
|
||
getContactStatement,
|
||
listContacts,
|
||
setContactArchived,
|
||
updateContact,
|
||
updateContactEntry,
|
||
} from "./service";
|
||
|
||
let ids: Awaited<ReturnType<typeof setupSeededDb>>;
|
||
let cakir = "";
|
||
let ayse = "";
|
||
let emre = "";
|
||
let zeynep = "";
|
||
let fatma = "";
|
||
const today = todayIstanbul();
|
||
|
||
beforeAll(async () => {
|
||
ids = await setupSeededDb();
|
||
cakir = ids.household("Çakır Ailesi");
|
||
ayse = ids.user("ayse");
|
||
emre = ids.user("emre");
|
||
zeynep = ids.user("zeynep");
|
||
fatma = ids.user("fatma");
|
||
});
|
||
afterAll(teardownDb);
|
||
|
||
const acc = (name: string) =>
|
||
AccountModel.findOne({ householdId: cakir, name })
|
||
.lean()
|
||
.then((a) => String(a!._id));
|
||
const contactForm = (over: Partial<ContactFormInput> = {}) =>
|
||
contactFormSchema.parse({ name: "Test Kişi", openingDate: today, ...over });
|
||
const entry = (over: Partial<ContactEntryInput>) =>
|
||
contactEntrySchema.parse({ kind: "lend", amount: "1000", date: today, ...over });
|
||
|
||
async function balance(userId: string, accountId: string) {
|
||
return (await listAccounts(userId, cakir))!.accounts.find((a) => a.id === accountId)!
|
||
.balanceMinor!;
|
||
}
|
||
async function contactBalance(userId: string, id: string) {
|
||
return (await listContacts(userId, cakir))!.contacts.find((c) => c.id === id)!.balanceMinor;
|
||
}
|
||
const sums = async () =>
|
||
(await listTransactions(ayse, cakir, transactionFilterSchema.parse({})))!.sums;
|
||
|
||
describe("cari defteri", () => {
|
||
let ahmet = "";
|
||
let lendId = "";
|
||
|
||
it("borç verme ve tahsilat: hesap bakiyeleri değişir, gelir/gider toplamlarına girmez", async () => {
|
||
const ziraat = await acc("Ziraat Maaş");
|
||
const cuzdan = await acc("Cüzdan");
|
||
const [zBefore, cBefore, sBefore] = [
|
||
await balance(ayse, ziraat),
|
||
await balance(ayse, cuzdan),
|
||
await sums(),
|
||
];
|
||
const c = await createContact(
|
||
ayse,
|
||
cakir,
|
||
contactForm({ name: "Kemal Test", relation: "friend" }),
|
||
);
|
||
expect(c.ok).toBe(true);
|
||
ahmet = c.ok ? c.data.id : "";
|
||
|
||
const l = await createContactEntry(
|
||
ayse,
|
||
cakir,
|
||
ahmet,
|
||
entry({ accountId: ziraat, amount: "5.000" }),
|
||
);
|
||
expect(l.ok).toBe(true);
|
||
lendId = l.ok ? l.data.id : "";
|
||
await createContactEntry(
|
||
emre,
|
||
cakir,
|
||
ahmet,
|
||
entry({ kind: "collect", accountId: cuzdan, amount: "2.000" }),
|
||
);
|
||
|
||
expect(await balance(ayse, ziraat)).toBe(zBefore - 5_000_00);
|
||
expect(await balance(ayse, cuzdan)).toBe(cBefore + 2_000_00);
|
||
expect(await contactBalance(ayse, ahmet)).toBe(3_000_00);
|
||
expect(await sums()).toEqual(sBefore);
|
||
|
||
const detail = (await getTransaction(ayse, cakir, lendId))!;
|
||
expect(detail).toMatchObject({
|
||
type: "transfer",
|
||
contact: { kind: "lend", name: "Kemal Test" },
|
||
});
|
||
expect(detail.toAccount).toBeNull();
|
||
});
|
||
|
||
it("borç alma ve geri ödeme; açılış bakiyesi; ekstre yürüyen bakiye", async () => {
|
||
const ziraat = await acc("Ziraat Maaş");
|
||
const c = await createContact(
|
||
ayse,
|
||
cakir,
|
||
contactForm({ name: "Hasan Amca", relation: "family", openingBalance: "-1.000" }),
|
||
);
|
||
const hasan = c.ok ? c.data.id : "";
|
||
await createContactEntry(
|
||
ayse,
|
||
cakir,
|
||
hasan,
|
||
entry({ kind: "borrow", accountId: ziraat, amount: "10.000" }),
|
||
);
|
||
await createContactEntry(
|
||
ayse,
|
||
cakir,
|
||
hasan,
|
||
entry({ kind: "repay", accountId: ziraat, amount: "4.000" }),
|
||
);
|
||
expect(await contactBalance(ayse, hasan)).toBe(-7_000_00);
|
||
|
||
const s = (await getContactStatement(ayse, cakir, hasan))!;
|
||
expect(s.rows).toHaveLength(2);
|
||
expect(s.rows[0]!.balanceMinor).toBe(-7_000_00);
|
||
expect(s.rows[1]!.balanceMinor).toBe(-11_000_00);
|
||
expect(s.contact.balanceMinor).toBe(-7_000_00);
|
||
|
||
const list = (await listContacts(ayse, cakir))!;
|
||
expect(list.totals.iOweMinor).toBeGreaterThanOrEqual(7_000_00);
|
||
expect(list.totals.owesMeMinor).toBeGreaterThanOrEqual(3_000_00);
|
||
});
|
||
|
||
it("hareket düzenleme: tür değişince yön ve bakiyeler değişir; işlem formuyla düzenlenemez", async () => {
|
||
const ziraat = await acc("Ziraat Maaş");
|
||
const before = await balance(ayse, ziraat);
|
||
const u = await updateContactEntry(
|
||
ayse,
|
||
cakir,
|
||
lendId,
|
||
entry({ kind: "borrow", accountId: ziraat, amount: "5.000" }),
|
||
);
|
||
expect(u.ok).toBe(true);
|
||
expect(await balance(ayse, ziraat)).toBe(before + 10_000_00);
|
||
expect(await contactBalance(ayse, ahmet)).toBe(-7_000_00);
|
||
const d = (await getTransaction(ayse, cakir, lendId))!;
|
||
expect(d.account).toBeNull();
|
||
expect(d.toAccount?.id).toBe(ziraat);
|
||
|
||
const viaForm = await updateTransaction(ayse, cakir, lendId, {
|
||
type: "transfer",
|
||
amount: 1,
|
||
date: today,
|
||
accountId: ziraat,
|
||
toAccountId: await acc("Cüzdan"),
|
||
categoryId: null,
|
||
splits: [],
|
||
payee: "",
|
||
description: "",
|
||
tags: [],
|
||
installments: 1,
|
||
attachmentIds: [],
|
||
});
|
||
expect(viaForm.ok).toBe(false);
|
||
});
|
||
|
||
it("vade uyarısı: yaklaşan, geçmiş, kapalı bakiye", async () => {
|
||
const c = await createContact(
|
||
ayse,
|
||
cakir,
|
||
contactForm({
|
||
name: "Vade Kişi",
|
||
openingBalance: "500",
|
||
dueDate: addDays(today, 2),
|
||
reminderDays: 3,
|
||
}),
|
||
);
|
||
const id = c.ok ? c.data.id : "";
|
||
const before = await countDueContacts(ayse, cakir);
|
||
let v = (await listContacts(ayse, cakir))!.contacts.find((x) => x.id === id)!;
|
||
expect(v.due).toBe("upcoming");
|
||
await updateContact(
|
||
ayse,
|
||
cakir,
|
||
id,
|
||
contactForm({ name: "Vade Kişi", openingBalance: "500", dueDate: addDays(today, -1) }),
|
||
);
|
||
v = (await listContacts(ayse, cakir))!.contacts.find((x) => x.id === id)!;
|
||
expect(v.due).toBe("overdue");
|
||
await updateContact(
|
||
ayse,
|
||
cakir,
|
||
id,
|
||
contactForm({ name: "Vade Kişi", openingBalance: "0", dueDate: addDays(today, -1) }),
|
||
);
|
||
v = (await listContacts(ayse, cakir))!.contacts.find((x) => x.id === id)!;
|
||
expect(v.due).toBe("none");
|
||
expect(await countDueContacts(ayse, cakir)).toBe(before - 1);
|
||
});
|
||
|
||
it("aynı ad engellenir; arşivli cariye hareket eklenmez", async () => {
|
||
expect((await createContact(ayse, cakir, contactForm({ name: "kemal TEST" }))).ok).toBe(false);
|
||
expect((await setContactArchived(ayse, cakir, ahmet, true)).ok).toBe(true);
|
||
expect(
|
||
(await createContactEntry(ayse, cakir, ahmet, entry({ accountId: await acc("Cüzdan") }))).ok,
|
||
).toBe(false);
|
||
expect((await setContactArchived(ayse, cakir, ahmet, false)).ok).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("yetki ve gizlilik", () => {
|
||
it("izleyici yazamaz; hane dışı göremez; başka hanenin hesabı kullanılamaz", async () => {
|
||
expect((await createContact(zeynep, cakir, contactForm({ name: "X" }))).ok).toBe(false);
|
||
expect(await listContacts(fatma, cakir)).toBeNull();
|
||
const c = await createContact(ayse, cakir, contactForm({ name: "Hesap Testi" }));
|
||
const id = c.ok ? c.data.id : "";
|
||
const otherHousehold = String(
|
||
(await AccountModel.findOne({ name: "Vakıfbank Emekli" }).lean())!._id,
|
||
);
|
||
expect(
|
||
(await createContactEntry(ayse, cakir, id, entry({ accountId: otherHousehold }))).ok,
|
||
).toBe(false);
|
||
});
|
||
|
||
it("yalnızca ben cari: başkası listede, ekstrede ve işlemlerde görmez; hesap ekstresinde ad gizlenir", async () => {
|
||
const ziraat = await acc("Ziraat Maaş");
|
||
const c = await createContact(
|
||
ayse,
|
||
cakir,
|
||
contactForm({ name: "Gizli Kişi", isPrivate: true }),
|
||
);
|
||
const id = c.ok ? c.data.id : "";
|
||
const e = await createContactEntry(
|
||
ayse,
|
||
cakir,
|
||
id,
|
||
entry({ accountId: ziraat, amount: "750" }),
|
||
);
|
||
const txId = e.ok ? e.data.id : "";
|
||
|
||
expect((await listContacts(emre, cakir))!.contacts.some((x) => x.id === id)).toBe(false);
|
||
expect(await getContactStatement(emre, cakir, id)).toBeNull();
|
||
expect(await getTransaction(emre, cakir, txId)).toBeNull();
|
||
expect((await createContactEntry(emre, cakir, id, entry({ accountId: ziraat }))).ok).toBe(
|
||
false,
|
||
);
|
||
|
||
const st = (await getAccountStatement(emre, cakir, ziraat, currentMonth()))!;
|
||
const row = st.rows.find((r) => r.item.id === txId)!;
|
||
expect(row.restricted).toBe(true);
|
||
expect(row.item.contact!.name).not.toBe("Gizli Kişi");
|
||
// Ortak hesabın bakiyesi herkes için aynı.
|
||
expect(await balance(emre, ziraat)).toBe(await balance(ayse, ziraat));
|
||
});
|
||
});
|