5bd2c372bf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
5.4 KiB
TypeScript
202 lines
5.4 KiB
TypeScript
import type { Types } from "mongoose";
|
||
import { createRule } from "@/features/recurring/service";
|
||
import { AccountModel } from "@/models/account";
|
||
import { CategoryModel } from "@/models/category";
|
||
import { RecurringRuleModel } from "@/models/recurring-rule";
|
||
import { todayIstanbul } from "@/shared/dates";
|
||
import { addDays, addMonths } from "@/shared/recurrence";
|
||
import { recurringRuleFormSchema, type RecurringRuleFormInput } from "@/shared/schemas/recurring";
|
||
import { formatMoneyInput } from "@/shared/money";
|
||
|
||
/**
|
||
* Örnek düzenli işlemler (Faz 7a). Seed işlemleri bugüne kadarki günleri doldurur; kurallar bir
|
||
* sonraki gerçekleşme gününden başlar (çakışma yok). "Spor salonu" kasıtlı olarak 3 gün önce
|
||
* başlar: vadesi geçmiş planlı kayıt örneği. İdempotent (`seedRef`).
|
||
*/
|
||
type SeedRule = {
|
||
ref: string;
|
||
by: string;
|
||
type: "income" | "expense" | "transfer";
|
||
amountMinor: number;
|
||
account: string;
|
||
category?: string;
|
||
payee: string;
|
||
/** Ayın günü (bir sonraki gerçekleşme) veya bugüne göre gün farkı. */
|
||
day?: number;
|
||
offsetDays?: number;
|
||
isEstimate?: boolean;
|
||
count?: number;
|
||
};
|
||
|
||
const RULES: Record<string, SeedRule[]> = {
|
||
"Çakır Ailesi": [
|
||
{
|
||
ref: "maas",
|
||
by: "admin",
|
||
type: "income",
|
||
amountMinor: 52_000_00,
|
||
account: "Ziraat Maaş",
|
||
category: "maas",
|
||
payee: "Anadolu Yazılım A.Ş.",
|
||
day: 15,
|
||
},
|
||
{
|
||
ref: "kira",
|
||
by: "admin",
|
||
type: "expense",
|
||
amountMinor: 17_500_00,
|
||
account: "Ziraat Maaş",
|
||
category: "kira",
|
||
payee: "Ev sahibi",
|
||
day: 3,
|
||
},
|
||
{
|
||
ref: "aidat",
|
||
by: "admin",
|
||
type: "expense",
|
||
amountMinor: 1_250_00,
|
||
account: "Ziraat Maaş",
|
||
category: "aidat",
|
||
payee: "Site yönetimi",
|
||
day: 5,
|
||
},
|
||
{
|
||
ref: "elektrik",
|
||
by: "ayse",
|
||
type: "expense",
|
||
amountMinor: 1_200_00,
|
||
account: "Ziraat Maaş",
|
||
category: "faturalar.elektrik",
|
||
payee: "Enerjisa",
|
||
day: 10,
|
||
isEstimate: true,
|
||
},
|
||
{
|
||
ref: "su",
|
||
by: "ayse",
|
||
type: "expense",
|
||
amountMinor: 450_00,
|
||
account: "Ziraat Maaş",
|
||
category: "faturalar.su",
|
||
payee: "İSKİ",
|
||
day: 12,
|
||
isEstimate: true,
|
||
},
|
||
{
|
||
ref: "dogalgaz",
|
||
by: "ayse",
|
||
type: "expense",
|
||
amountMinor: 2_500_00,
|
||
account: "Ziraat Maaş",
|
||
category: "faturalar.dogalgaz",
|
||
payee: "İGDAŞ",
|
||
day: 14,
|
||
isEstimate: true,
|
||
},
|
||
{
|
||
ref: "internet",
|
||
by: "admin",
|
||
type: "expense",
|
||
amountMinor: 499_90,
|
||
account: "Garanti Bonus",
|
||
category: "faturalar.internet",
|
||
payee: "Türk Telekom",
|
||
day: 20,
|
||
},
|
||
{
|
||
ref: "netflix",
|
||
by: "admin",
|
||
type: "expense",
|
||
amountMinor: 229_99,
|
||
account: "Garanti Bonus",
|
||
category: "abonelikler",
|
||
payee: "Netflix",
|
||
day: 8,
|
||
},
|
||
{
|
||
ref: "spor",
|
||
by: "emre",
|
||
type: "expense",
|
||
amountMinor: 1_250_00,
|
||
account: "Garanti Bonus",
|
||
category: "eglence",
|
||
payee: "Spor salonu",
|
||
offsetDays: -3,
|
||
count: 12,
|
||
},
|
||
],
|
||
Annemler: [
|
||
{
|
||
ref: "emekli",
|
||
by: "fatma",
|
||
type: "income",
|
||
amountMinor: 24_350_00,
|
||
account: "Vakıfbank Emekli",
|
||
category: "maas",
|
||
payee: "SGK",
|
||
day: 17,
|
||
},
|
||
{
|
||
ref: "elektrik",
|
||
by: "fatma",
|
||
type: "expense",
|
||
amountMinor: 700_00,
|
||
account: "Vakıfbank Emekli",
|
||
category: "faturalar.elektrik",
|
||
payee: "Enerjisa",
|
||
day: 11,
|
||
isEstimate: true,
|
||
},
|
||
],
|
||
};
|
||
|
||
/** Bugünden sonraki ilk "ayın N. günü". */
|
||
function nextMonthDay(today: string, day: number): string {
|
||
const thisMonth = addMonths(today, 0, day);
|
||
return thisMonth > today ? thisMonth : addMonths(today, 1, day);
|
||
}
|
||
|
||
export async function seedRecurring(
|
||
householdIds: Map<string, Types.ObjectId>,
|
||
userIds: Map<string, string>,
|
||
): Promise<number> {
|
||
const today = todayIstanbul();
|
||
let created = 0;
|
||
for (const [household, rules] of Object.entries(RULES)) {
|
||
const householdId = householdIds.get(household)!;
|
||
const [accounts, categories] = await Promise.all([
|
||
AccountModel.find({ householdId }).select({ name: 1 }).lean(),
|
||
CategoryModel.find({ householdId, defaultKey: { $ne: null } })
|
||
.select({ defaultKey: 1 })
|
||
.lean(),
|
||
]);
|
||
for (const r of rules) {
|
||
const seedRef = `rule:${r.ref}`;
|
||
if (await RecurringRuleModel.exists({ householdId, seedRef })) continue;
|
||
const input: RecurringRuleFormInput = {
|
||
type: r.type,
|
||
amount: formatMoneyInput(r.amountMinor),
|
||
isEstimate: r.isEstimate ?? false,
|
||
accountId: String(accounts.find((a) => a.name === r.account)!._id),
|
||
categoryId: r.category
|
||
? String(categories.find((c) => c.defaultKey === r.category)!._id)
|
||
: null,
|
||
payee: r.payee,
|
||
startDate: r.day ? nextMonthDay(today, r.day) : addDays(today, r.offsetDays ?? 0),
|
||
frequency: "monthly",
|
||
endKind: r.count ? "count" : "never",
|
||
count: r.count ?? null,
|
||
};
|
||
const result = await createRule(
|
||
userIds.get(r.by)!,
|
||
String(householdId),
|
||
recurringRuleFormSchema.parse(input),
|
||
);
|
||
if (!result.ok) throw new Error(`Seed kuralı oluşturulamadı (${seedRef}): ${result.error}`);
|
||
await RecurringRuleModel.updateOne({ _id: result.data.id }, { $set: { seedRef } });
|
||
created++;
|
||
}
|
||
}
|
||
return created;
|
||
}
|