diff --git a/CHANGELOG.md b/CHANGELOG.md index d39d03d..c08d848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Biçim [Keep a Changelog](https://keepachangelog.com/tr-TR/1.1.0/) esas alınır ## [Yayınlanmamış] +### Eklendi — Faz 10a: aylık bütçe + +- Bütçe sayfası (`/butce?ay=YYYY-MM`): ay gezintisi, dönem sınırları hane ayarı `budgetMonthStartDay`'e göre (ör. 15'i → 15 Kasım – 14 Aralık). +- Kategori limitleri (`budgets`): her gider kategorisine limit; üst kategori limiti alt kategorileri kapsar, alt kategori limiti üstün içinde alt sınırdır ve toplam bütçede iki kez sayılmaz. Limitsiz alt kategoriler "Üst kategori limitinde" olarak gösterilir; hiçbir limitin kapsamadığı harcama "Bütçesiz harcama" toplamında. +- Gerçekleşen / limit / kalan, yüzde çubuğu (ayın geçen kısmı işaretli), %80'de uyarı, %100'de "Limit doldu", aşımda "Limit aşıldı". +- Ay sonu tahmini: sabit giderler (düzenli işlem, taksit) olduğu gibi + kalan planlı giderler + değişken giderin günlük hızıyla ay sonuna yansıtılması; tahmin limiti aşıyorsa işaret. +- Bölünmüş işlemler kalem bazında; taksitler hane ayarına göre her biri kendi ayına (varsayılan) veya tamamı alış ayına. Başkasının "yalnızca ben" hesabından giderler hane ayarına göre yalnızca tutar olarak sayılır. +- "Önceki aydan kopyala" ve "Önceki ayın harcamalarından oluştur" (ana kategori bazında, 100 TL'ye yukarı yuvarlanır); mevcut limitler varsa onay istenir. +- Limit düzenleyici: tüm etkin gider kategorileri ağaç sırasıyla, anlık toplam bütçe. +- Dashboard: %80'i geçilen veya aşılan kategori sayısıyla uygulama içi bütçe uyarısı. +- Seed: Çakır Ailesi'nde önceki ve bu ay için harcamalardan oluşturulmuş bütçeler (bu ay Market limiti düşük, Elektrik alt limiti). +- `010` migration'ı: `budgets` index'i (hane × ay benzersiz). +- Testler: dönem sınırları, eşikler, tahmin, üst/alt limit toplama, bütçesiz harcama, sıra (birim); limitler, bölünmüş işlem, planlı, gizli hesap ayarı, taksit modları, başlangıç günü, kopyala/oluştur, yetki (entegrasyon). + ## [0.9.0] - 2026-11-06 Faz 9: birikim ve varlık takibi, fiyat tablosu (elle), kâr/zarar, net varlık (9a ve 9b alt fazları). diff --git a/scripts/seed/budgets.ts b/scripts/seed/budgets.ts new file mode 100644 index 0000000..7c76714 --- /dev/null +++ b/scripts/seed/budgets.ts @@ -0,0 +1,50 @@ +import type { Types } from "mongoose"; +import { budgetFromActuals, saveBudget } from "@/features/budgets/service"; +import { BudgetModel } from "@/models/budget"; +import { CategoryModel } from "@/models/category"; +import { HouseholdModel } from "@/models/household"; +import { periodOfDate, shiftMonth } from "@/shared/budget"; +import { todayIstanbul } from "@/shared/dates"; +import { budgetLimitsSchema } from "@/shared/schemas/budget"; +import { formatMoneyInput } from "@/shared/money"; + +/** + * Örnek bütçeler (Faz 10a): Çakır Ailesi'nde önceki ve bu ay, önceki ayların harcamalarından + * oluşturulur. Bu ay "Market" limiti biraz düşürülür ve "Elektrik" alt limiti eklenir; böylece + * uyarı ve alt limit örnekleri görünür. İdempotent: bütçesi olan aya dokunulmaz. + */ +export async function seedBudgets( + householdIds: Map, + userIds: Map, +): Promise { + const householdId = householdIds.get("Çakır Ailesi")!; + const by = userIds.get("ayse")!; + const hh = String(householdId); + const h = await HouseholdModel.findById(householdId).select({ settings: 1 }).lean(); + const current = periodOfDate(todayIstanbul(), h?.settings?.budgetMonthStartDay ?? 1); + let created = 0; + for (const month of [shiftMonth(current, -1), current]) { + if (await BudgetModel.exists({ householdId, month })) continue; + const r = await budgetFromActuals(by, hh, month); + if (!r.ok) continue; + created++; + } + if (created && (await BudgetModel.exists({ householdId, month: current }))) { + const doc = (await BudgetModel.findOne({ householdId, month: current }).lean())!; + const cats = await CategoryModel.find({ + householdId, + defaultKey: { $in: ["market", "faturalar.elektrik"] }, + }).lean(); + const id = (key: string) => String(cats.find((c) => c.defaultKey === key)?._id ?? ""); + const limits = doc.limits.map((l) => ({ + categoryId: String(l.categoryId), + amount: formatMoneyInput( + String(l.categoryId) === id("market") ? Math.round(l.amountMinor * 0.7) : l.amountMinor, + ), + })); + if (id("faturalar.elektrik")) + limits.push({ categoryId: id("faturalar.elektrik"), amount: "900" }); + await saveBudget(by, hh, budgetLimitsSchema.parse({ month: current, limits })); + } + return created; +} diff --git a/scripts/seed/run.int.test.ts b/scripts/seed/run.int.test.ts index 2c9b0ee..a60c5d7 100644 --- a/scripts/seed/run.int.test.ts +++ b/scripts/seed/run.int.test.ts @@ -33,6 +33,7 @@ describe("seed", () => { expect(summary.created.accounts).toBe(7); expect(summary.created.transactions).toBeGreaterThan(150); expect(summary.created.assets).toBe(4); + expect(summary.created.budgets).toBe(2); expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1); expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount); }); @@ -51,6 +52,7 @@ describe("seed", () => { contacts: 0, inKind: 0, assets: 0, + budgets: 0, }); expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1); expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length); diff --git a/scripts/seed/run.ts b/scripts/seed/run.ts index 6cff6c8..165cbfa 100644 --- a/scripts/seed/run.ts +++ b/scripts/seed/run.ts @@ -12,6 +12,7 @@ import { storedEmail } from "@/shared/account-email"; import { runMigrations } from "../migrations"; import { seedContacts } from "./contacts"; import { seedAssets } from "./assets"; +import { seedBudgets } from "./budgets"; import { seedInKind } from "./in-kind"; import { seedInstallments } from "./installments"; import { seedRecurring } from "./recurring"; @@ -53,6 +54,7 @@ export type SeedSummary = { contacts: number; inKind: number; assets: number; + budgets: number; }; accounts: SeedAccountRow[]; }; @@ -75,6 +77,7 @@ const DOMAIN_COLLECTIONS = [ "inKindDebts", "assets", "assetMovements", + "budgets", ]; type AuthApi = ReturnType["api"]; @@ -129,6 +132,7 @@ export async function runSeed({ contacts: 0, inKind: 0, assets: 0, + budgets: 0, }, accounts: [], }; @@ -309,6 +313,8 @@ export async function runSeed({ if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`); summary.created.assets = await seedAssets(householdIds, userIdMap); if (summary.created.assets) log(`✓ Örnek varlıklar: ${summary.created.assets}`); + summary.created.budgets = await seedBudgets(householdIds, userIdMap); + if (summary.created.budgets) log(`✓ Örnek bütçeler: ${summary.created.budgets}`); // 7) Geliştirme hesapları tablosu const all = [