fix(budget): sabit gider ayrımı ve taban hızlı ay sonu tahmini
$ne ile eksik alan yolu her işlemi sabit sayıyordu; $ifNull ile düzeltildi. Tahmin, bu ayın hızını önceki dönemin değişken hızıyla karıştırır. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -206,6 +206,30 @@ describe("aylık bütçe", () => {
|
||||
expect((await line("aidat", shiftMonth(M, 1)))!.actualMinor).toBe(1_000_00);
|
||||
});
|
||||
|
||||
it("tahmin: değişken gider tempoyla, sabit gider olduğu gibi (gelecek ayda değişken sayılmaz)", async () => {
|
||||
const F = shiftMonth(todayIstanbul().slice(0, 7), 24);
|
||||
const f = budgetPeriod(F);
|
||||
await expense({ date: f.from, amountMinor: 900_00, category: cat.keyif });
|
||||
await expense({ date: f.from, amountMinor: 4_000_00, category: cat.kira, recurring: true });
|
||||
const keyif = (await line("keyif", F))!;
|
||||
expect(keyif.actualMinor).toBe(900_00);
|
||||
// Henüz başlamamış dönem, geçmiş yok: gerçekleşen değişken olduğu gibi, tempo eklenmez.
|
||||
expect(keyif.forecastMinor).toBe(900_00);
|
||||
expect((await line("kira", F))!.forecastMinor).toBe(4_000_00);
|
||||
});
|
||||
|
||||
it("içinde bulunulan ayda değişken gider tempoyla büyür, sabit gider büyümez", async () => {
|
||||
const today = todayIstanbul();
|
||||
const cur = today.slice(0, 7);
|
||||
if (budgetPeriod(cur).to === today) return; // ayın son günü: kalan gün yok
|
||||
await expense({ date: today, amountMinor: 1_000_00, category: cat.keyif });
|
||||
await expense({ date: today, amountMinor: 2_000_00, category: cat.kira, recurring: true });
|
||||
const keyif = (await line("keyif", cur))!;
|
||||
expect(keyif.actualMinor).toBe(1_000_00);
|
||||
expect(keyif.forecastMinor).toBeGreaterThan(1_000_00);
|
||||
expect((await line("kira", cur))!.forecastMinor).toBe(2_000_00);
|
||||
});
|
||||
|
||||
it("bütçe ayı başlangıç günü dönemi kaydırır", async () => {
|
||||
await HouseholdModel.updateOne(
|
||||
{ _id: cakir },
|
||||
|
||||
@@ -34,18 +34,20 @@ import type { BudgetLimitsForm } from "@/shared/schemas/budget";
|
||||
const t = tr.budget;
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
type Settings = {
|
||||
export type Settings = {
|
||||
startDay: number;
|
||||
installmentMode: "per_installment" | "purchase_month";
|
||||
includePrivate: boolean;
|
||||
dailyMode: "even" | "previous_month_pattern";
|
||||
};
|
||||
|
||||
async function settingsOf(householdId: Types.ObjectId): Promise<Settings> {
|
||||
export async function settingsOf(householdId: Types.ObjectId): Promise<Settings> {
|
||||
const h = await HouseholdModel.findById(householdId).select({ settings: 1 }).lean();
|
||||
return {
|
||||
startDay: h?.settings?.budgetMonthStartDay ?? 1,
|
||||
installmentMode: h?.settings?.installmentBudgetMode ?? "per_installment",
|
||||
includePrivate: h?.settings?.includePrivateAccountsInTotals ?? true,
|
||||
dailyMode: h?.settings?.dailyPlanMode ?? "even",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +72,10 @@ export async function expenseAmounts(
|
||||
$project: {
|
||||
status: 1,
|
||||
fixed: {
|
||||
$or: [{ $ne: ["$recurringRuleId", null] }, { $ne: ["$installment.planId", null] }],
|
||||
$or: [
|
||||
{ $ne: [{ $ifNull: ["$recurringRuleId", null] }, null] },
|
||||
{ $ne: [{ $ifNull: ["$installment.planId", null] }, null] },
|
||||
],
|
||||
},
|
||||
items: {
|
||||
$cond: [
|
||||
@@ -205,17 +210,32 @@ export async function getBudget(
|
||||
expenseAmounts(ctx, s, period.from, period.to),
|
||||
]);
|
||||
const limits = new Map((doc?.limits ?? []).map((l) => [String(l.categoryId), l.amountMinor]));
|
||||
// Taban hız (bitmemiş aylar için): önceki dönemin değişken gideri, kategori bazında.
|
||||
let baselineDays: number | undefined;
|
||||
if (elapsedDays < totalDays) {
|
||||
const prev = budgetPeriod(shiftMonth(m, -1), s.startDay);
|
||||
const prevAmounts = await expenseAmounts(ctx, s, prev.from, prev.to);
|
||||
let any = 0;
|
||||
for (const [id, a] of prevAmounts) {
|
||||
if (a.variableMinor <= 0) continue;
|
||||
any += a.variableMinor;
|
||||
const cur = amounts.get(id) ?? { fixedMinor: 0, variableMinor: 0, plannedMinor: 0 };
|
||||
amounts.set(id, { ...cur, baselineMinor: a.variableMinor });
|
||||
}
|
||||
if (any > 0) baselineDays = daysBetween(prev.from, prev.to);
|
||||
}
|
||||
const cats = expenseCategories(ctx);
|
||||
// Arşivli kategori yalnızca limiti veya harcaması varsa görünür.
|
||||
const visibleCats = cats.filter((c) => !c.archived || limits.has(c.id) || amounts.has(c.id));
|
||||
const known = new Set(visibleCats.map((c) => c.id));
|
||||
// Kategorisiz / bilinmeyen kategorideki gider "Kategorisiz" satırında toplanır.
|
||||
const orphan = { fixedMinor: 0, variableMinor: 0, plannedMinor: 0 };
|
||||
const orphan = { fixedMinor: 0, variableMinor: 0, plannedMinor: 0, baselineMinor: 0 };
|
||||
for (const [id, a] of amounts)
|
||||
if (!known.has(id)) {
|
||||
orphan.fixedMinor += a.fixedMinor;
|
||||
orphan.variableMinor += a.variableMinor;
|
||||
orphan.plannedMinor += a.plannedMinor;
|
||||
orphan.baselineMinor += a.baselineMinor ?? 0;
|
||||
}
|
||||
const ORPHAN = "__none__";
|
||||
const inputCats = visibleCats.map((c) => ({
|
||||
@@ -234,6 +254,7 @@ export async function getBudget(
|
||||
amounts,
|
||||
elapsedDays,
|
||||
totalDays,
|
||||
baselineDays,
|
||||
});
|
||||
const editorCats = cats.filter((c) => !c.archived);
|
||||
// Düzenleyici için ağaç sırası ve derinlik (tüm etkin kategoriler, limit 0 ile).
|
||||
|
||||
Reference in New Issue
Block a user