From 5bd2c372bff2079e2e262395fa20040dcd52e94a Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 10:55:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(seed):=20=C3=B6rnek=20d=C3=BCzenli=20i?= =?UTF-8?q?=C5=9Flemler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/seed/recurring.ts | 201 +++++++++++++++++++++++++++++++++++ scripts/seed/run.int.test.ts | 1 + scripts/seed/run.ts | 7 ++ 3 files changed, 209 insertions(+) create mode 100644 scripts/seed/recurring.ts diff --git a/scripts/seed/recurring.ts b/scripts/seed/recurring.ts new file mode 100644 index 0000000..33ca568 --- /dev/null +++ b/scripts/seed/recurring.ts @@ -0,0 +1,201 @@ +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 = { + "Ç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, + userIds: Map, +): Promise { + 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; +} diff --git a/scripts/seed/run.int.test.ts b/scripts/seed/run.int.test.ts index b720dc8..26e97a3 100644 --- a/scripts/seed/run.int.test.ts +++ b/scripts/seed/run.int.test.ts @@ -45,6 +45,7 @@ describe("seed", () => { categories: 0, accounts: 0, transactions: 0, + recurringRules: 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 1bb65f7..348f7e2 100644 --- a/scripts/seed/run.ts +++ b/scripts/seed/run.ts @@ -10,6 +10,7 @@ import { HouseholdModel } from "@/models/household"; import { HouseholdMemberModel } from "@/models/household-member"; import { storedEmail } from "@/shared/account-email"; import { runMigrations } from "../migrations"; +import { seedRecurring } from "./recurring"; import { seedTransactions } from "./transactions"; import { ADMIN_USERNAME, @@ -43,6 +44,7 @@ export type SeedSummary = { categories: number; accounts: number; transactions: number; + recurringRules: number; }; accounts: SeedAccountRow[]; }; @@ -58,6 +60,7 @@ const DOMAIN_COLLECTIONS = [ "payees", "attachments", "reconciliations", + "recurringRules", ]; type AuthApi = ReturnType["api"]; @@ -107,6 +110,7 @@ export async function runSeed({ categories: 0, accounts: 0, transactions: 0, + recurringRules: 0, }, accounts: [], }; @@ -272,6 +276,9 @@ export async function runSeed({ const userIdMap = new Map([...ids.entries()]); summary.created.transactions = await seedTransactions(householdIds, userIdMap); if (summary.created.transactions) log(`✓ Örnek işlemler: ${summary.created.transactions}`); + summary.created.recurringRules = await seedRecurring(householdIds, userIdMap); + if (summary.created.recurringRules) + log(`✓ Örnek düzenli işlemler: ${summary.created.recurringRules}`); // 7) Geliştirme hesapları tablosu const all = [