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>; 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 = {}) => contactFormSchema.parse({ name: "Test Kişi", openingDate: today, ...over }); const entry = (over: Partial) => 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)); }); });