import { Types } from "mongoose"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { AccountModel } from "@/models/account"; import { HouseholdModel } from "@/models/household"; import { TransactionModel } from "@/models/transaction"; import { budgetPeriod, periodOfDate, shiftMonth } from "@/shared/budget"; import { istanbulDayStart, todayIstanbul } from "@/shared/dates"; import { incomeItemsSchema } from "@/shared/schemas/budget"; import { setupSeededDb, teardownDb } from "@/test/seeded-db"; import { getDailyPlan, getIncomePlan, saveIncomeItems } from "./plan"; import { getBudget } from "./service"; let ids: Awaited>; let cakir = ""; let ayse = ""; let emre = ""; let zeynep = ""; let fatma = ""; const today = todayIstanbul(); const month = periodOfDate(today); 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); async function income( date: string, amountMinor: number, status: "posted" | "planned", accountId?: Types.ObjectId, ) { const actor = new Types.ObjectId(ayse); const acc = accountId ?? (await AccountModel.findOne({ householdId: cakir, name: "Ziraat Maaş" }).lean())!._id; await TransactionModel.create({ householdId: new Types.ObjectId(cakir), type: "income", status, date: istanbulDayStart(date)!, accountId: acc, amountMinor, description: "Test gelir", recurringRuleId: status === "planned" ? new Types.ObjectId() : null, occurrenceIndex: status === "planned" ? 0 : null, createdBy: actor, updatedBy: actor, }); } describe("gelir planı", () => { it("gerçekleşen + bekleyen planlı + elle eklenen = beklenen", async () => { const p0 = (await getIncomePlan(ayse, cakir, month))!; const { to } = budgetPeriod(month); await income(to, 3_000_00, "planned"); expect( ( await saveIncomeItems( ayse, cakir, incomeItemsSchema.parse({ month, items: [{ name: "Prim", amount: "5.000" }] }), ) ).ok, ).toBe(true); const p = (await getIncomePlan(ayse, cakir, month))!; expect(p.pendingMinor).toBe(p0.pendingMinor + 3_000_00); expect(p.manual).toEqual([expect.objectContaining({ name: "Prim", amountMinor: 5_000_00 })]); expect(p.expectedMinor).toBe(p.actualMinor + p.pendingMinor + 5_000_00); expect(p.entries.some((e) => e.status === "planned" && e.amountMinor === 3_000_00)).toBe(true); // Elle eklenenler bütçe limitlerini bozmaz. expect((await getBudget(ayse, cakir, month))!.editor.limits).toBeTruthy(); }); it("gizli hesaba gelen gelir hane ayarına göre; izleyici yazamaz; başka hane göremez", async () => { const actor = new Types.ObjectId(emre); const secret = await AccountModel.create({ householdId: new Types.ObjectId(cakir), name: "Emre Gizli Gelir", type: "bank", openingBalanceMinor: 0, openingDate: istanbulDayStart(shiftMonth(month, -2) + "-01")!, isPrivate: true, ownerUserId: actor, createdBy: actor, updatedBy: actor, }); const a0 = (await getIncomePlan(ayse, cakir, month))!.actualMinor; await income(today, 1_000_00, "posted", secret._id); expect((await getIncomePlan(ayse, cakir, month))!.actualMinor).toBe(a0 + 1_000_00); await HouseholdModel.updateOne( { _id: cakir }, { $set: { "settings.includePrivateAccountsInTotals": false } }, ); expect((await getIncomePlan(ayse, cakir, month))!.actualMinor).toBe(a0); await HouseholdModel.updateOne( { _id: cakir }, { $set: { "settings.includePrivateAccountsInTotals": true } }, ); // Sahibi kendi gizli hesabını her zaman görür. expect((await getIncomePlan(emre, cakir, month))!.actualMinor).toBe(a0 + 1_000_00); expect( (await saveIncomeItems(zeynep, cakir, incomeItemsSchema.parse({ month, items: [] }))).ok, ).toBe(false); expect(await getIncomePlan(fatma, cakir, month)).toBeNull(); expect(incomeItemsSchema.safeParse({ month, items: [{ name: "", amount: "1" }] }).success).toBe( false, ); expect( incomeItemsSchema.safeParse({ month, items: [{ name: "X", amount: "0" }] }).success, ).toBe(false); }); }); describe("günlük plan", () => { it("plan toplamı bütçeye eşit; bugüne kadarki gerçekleşen bütçe motoruyla tutarlı; elde kalacak", async () => { const v = (await getDailyPlan(ayse, cakir))!; const b = (await getBudget(ayse, cakir))!; expect(v.month).toBe(month); expect(v.hasBudget).toBe(true); const last = v.plan.days.at(-1)!; expect(last.cumPlannedMinor).toBe(Math.max(b.totals.limitMinor, v.plan.fixedMinor)); expect(v.plan.days).toHaveLength(b.totalDays); expect(v.plan.todayIndex).toBe(b.elapsedDays - 1); // Bugünden sonraki gerçekleşmiş işlem yok: bugüne kadar harcanan = ayın gerçekleşeni. expect(v.plan.actualToDateMinor).toBe(b.totals.actualMinor); expect(v.projectedLeftMinor).toBe(v.income.expectedMinor - b.totals.forecastMinor); expect(v.plannedLeftMinor).toBe(v.income.expectedMinor - b.totals.limitMinor); expect(v.next.month).toBe(shiftMonth(month, 1)); }); it("önceki ay örüntüsü modu: dağılım değişir, toplam korunur; bütçesiz hane", async () => { const even = (await getDailyPlan(ayse, cakir))!; await HouseholdModel.updateOne( { _id: cakir }, { $set: { "settings.dailyPlanMode": "previous_month_pattern" } }, ); const pattern = (await getDailyPlan(ayse, cakir))!; await HouseholdModel.updateOne({ _id: cakir }, { $set: { "settings.dailyPlanMode": "even" } }); expect(pattern.mode).toBe("previous_month_pattern"); expect(pattern.plan.days.at(-1)!.cumPlannedMinor).toBe(even.plan.days.at(-1)!.cumPlannedMinor); expect(pattern.plan.days.map((d) => d.plannedMinor)).not.toEqual( even.plan.days.map((d) => d.plannedMinor), ); const annemler = ids.household("Annemler"); const a = (await getDailyPlan(fatma, annemler))!; expect(a.hasBudget).toBe(false); expect(await getDailyPlan(fatma, cakir)).toBeNull(); }); });