From 0558d922032965ebdb6bed37a0f47bf396285f45 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 02:57:58 +0300 Subject: [PATCH] =?UTF-8?q?feat(seed):=20varsay=C4=B1lan=20kategoriler,=20?= =?UTF-8?q?=C3=B6rnek=20hesaplar=20ve=20002=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/migrations/002-accounts-categories.ts | 19 ++++ scripts/migrations/index.ts | 3 +- scripts/seed.ts | 6 +- scripts/seed/fixtures.ts | 96 +++++++++++++++++++ scripts/seed/run.int.test.ts | 12 ++- scripts/seed/run.ts | 76 ++++++++++++++- 6 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 scripts/migrations/002-accounts-categories.ts diff --git a/scripts/migrations/002-accounts-categories.ts b/scripts/migrations/002-accounts-categories.ts new file mode 100644 index 0000000..ef4487e --- /dev/null +++ b/scripts/migrations/002-accounts-categories.ts @@ -0,0 +1,19 @@ +import { Types } from "mongoose"; +import { installDefaultCategories } from "@/features/categories/service"; +import { AccountModel } from "@/models/account"; +import { CategoryModel } from "@/models/category"; +import { HouseholdModel } from "@/models/household"; +import type { Migration } from "./types"; + +/** Hesap ve kategori index'leri + mevcut hanelere varsayılan kategori seti (idempotent). */ +export const migration: Migration = { + name: "002-accounts-categories", + description: "accounts/categories index'leri ve mevcut hanelere varsayılan kategoriler", + async up() { + await AccountModel.createIndexes(); + await CategoryModel.createIndexes(); + for (const h of await HouseholdModel.find().select({ _id: 1, createdBy: 1 }).lean()) { + await installDefaultCategories(h._id as Types.ObjectId, h.createdBy as Types.ObjectId); + } + }, +}; diff --git a/scripts/migrations/index.ts b/scripts/migrations/index.ts index 9c76e86..b92e442 100644 --- a/scripts/migrations/index.ts +++ b/scripts/migrations/index.ts @@ -1,10 +1,11 @@ import { getDb } from "@/lib/db/mongo"; import { connectMongoose } from "@/lib/db/mongoose"; import { migration as m001 } from "./001-initial-indexes"; +import { migration as m002 } from "./002-accounts-categories"; import type { Migration } from "./types"; /** Sıralı migration listesi. Yeni migration: dosyayı ekle ve buraya sırayla ekle. */ -export const MIGRATIONS: Migration[] = [m001]; +export const MIGRATIONS: Migration[] = [m001, m002]; const COLLECTION = "migrations"; diff --git a/scripts/seed.ts b/scripts/seed.ts index 901e8c0..b45a035 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -11,8 +11,10 @@ import { closeSeedConnections, runSeed } from "./seed/run"; async function main() { const summary = await runSeed({ reset: process.argv.includes("--reset") }); - const { users, households, memberships } = summary.created; - console.log(`\nOluşturulan: ${users} kullanıcı, ${households} hane, ${memberships} üyelik.`); + const { users, households, memberships, categories, accounts } = summary.created; + console.log( + `\nOluşturulan: ${users} kullanıcı, ${households} hane, ${memberships} üyelik, ${categories} kategori, ${accounts} hesap.`, + ); // Hesap tablosu yalnızca geliştirme ortamında gösterilir. if (env().NODE_ENV !== "production" && summary.accounts.length) { diff --git a/scripts/seed/fixtures.ts b/scripts/seed/fixtures.ts index c46fa81..bb5d5df 100644 --- a/scripts/seed/fixtures.ts +++ b/scripts/seed/fixtures.ts @@ -1,4 +1,6 @@ import type { HouseholdRole } from "@/features/households/types"; +import type { AccountType, MealCardProvider } from "@/shared/accounts"; +import type { IconKey, Swatch } from "@/shared/appearance"; import type { SystemRole } from "@/shared/schemas/common"; /** Örnek (geliştirme) kullanıcıları. E-postası olmayanlar kullanıcı adıyla giriş yapar. */ @@ -56,3 +58,97 @@ export const SEED_HOUSEHOLDS: SeedHousehold[] = [ ], }, ]; + +/** Örnek hesaplar (tutarlar kuruş; kart borcu negatif). Açılış: 6 ay önce. */ +export type SeedAccount = { + household: string; + name: string; + type: AccountType; + institution?: string; + openingBalanceMinor: number; + color: Swatch; + icon: IconKey; + owner?: string; + isPrivate?: boolean; + mealCardProvider?: MealCardProvider; + overdraft?: { limitMinor: number; monthlyRateBp: number; statementDay: number }; + creditCard?: { + limitMinor: number; + statementDay: number; + dueDays: number; + paymentAccount: string; + }; +}; + +export const SEED_ACCOUNTS: SeedAccount[] = [ + { + household: "Çakır Ailesi", + name: "Cüzdan", + type: "cash", + openingBalanceMinor: 2_350_00, + color: "green", + icon: "wallet", + }, + { + household: "Çakır Ailesi", + name: "Ziraat Maaş", + type: "bank", + institution: "Ziraat Bankası", + openingBalanceMinor: 18_420_50, + color: "red", + icon: "landmark", + overdraft: { limitMinor: 10_000_00, monthlyRateBp: 425, statementDay: 1 }, + }, + { + household: "Çakır Ailesi", + name: "Garanti Bonus", + type: "credit_card", + institution: "Garanti BBVA", + openingBalanceMinor: -6_480_75, + color: "teal", + icon: "credit-card", + creditCard: { + limitMinor: 60_000_00, + statementDay: 29, + dueDays: 10, + paymentAccount: "Ziraat Maaş", + }, + }, + { + household: "Çakır Ailesi", + name: "Birikim (Vadeli)", + type: "savings", + institution: "Ziraat Bankası", + openingBalanceMinor: 45_000_00, + color: "indigo", + icon: "piggy-bank", + }, + { + household: "Çakır Ailesi", + name: "Ayşe — Pluxee", + type: "meal_card", + mealCardProvider: "pluxee", + openingBalanceMinor: 3_120_00, + color: "lime", + icon: "utensils", + owner: "ayse", + isPrivate: true, + }, + { + household: "Annemler", + name: "Nakit", + type: "cash", + openingBalanceMinor: 1_500_00, + color: "green", + icon: "wallet", + }, + { + household: "Annemler", + name: "Vakıfbank Emekli", + type: "bank", + institution: "VakıfBank", + openingBalanceMinor: 27_800_00, + color: "amber", + icon: "landmark", + }, +]; diff --git a/scripts/seed/run.int.test.ts b/scripts/seed/run.int.test.ts index 187daf5..bc0032c 100644 --- a/scripts/seed/run.int.test.ts +++ b/scripts/seed/run.int.test.ts @@ -24,18 +24,26 @@ describe("seed", () => { it("ilk çalıştırmada admin, örnek kullanıcılar, haneler ve üyelikleri oluşturur", async () => { const summary = await runSeed({ log: silent }); const memberCount = SEED_HOUSEHOLDS.reduce((n, h) => n + h.members.length, 0); - expect(summary.created).toEqual({ + expect(summary.created).toMatchObject({ users: SEED_USERS.length + 1, households: SEED_HOUSEHOLDS.length, memberships: memberCount, }); + expect(summary.created.categories).toBeGreaterThan(40); + expect(summary.created.accounts).toBe(7); expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1); expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount); }); it("idempotenttir: ikinci çalıştırma kopya oluşturmaz", async () => { const summary = await runSeed({ log: silent }); - expect(summary.created).toEqual({ users: 0, households: 0, memberships: 0 }); + expect(summary.created).toEqual({ + users: 0, + households: 0, + memberships: 0, + categories: 0, + accounts: 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 da38c25..c8d1d00 100644 --- a/scripts/seed/run.ts +++ b/scripts/seed/run.ts @@ -3,11 +3,19 @@ import { getAuth } from "@/lib/auth"; import { getDb } from "@/lib/db/mongo"; import { connectMongoose } from "@/lib/db/mongoose"; import { env } from "@/lib/env"; +import { installDefaultCategories } from "@/features/categories/service"; +import { AccountModel } from "@/models/account"; import { HouseholdModel } from "@/models/household"; import { HouseholdMemberModel } from "@/models/household-member"; import { storedEmail } from "@/shared/account-email"; import { runMigrations } from "../migrations"; -import { ADMIN_USERNAME, SEED_HOUSEHOLDS, SEED_USERS, type SeedUser } from "./fixtures"; +import { + ADMIN_USERNAME, + SEED_ACCOUNTS, + SEED_HOUSEHOLDS, + SEED_USERS, + type SeedUser, +} from "./fixtures"; /** * İdempotent seed. Kullanıcılar Better Auth API'siyle oluşturulur (şifreler doğru hashlenir); @@ -26,12 +34,24 @@ export type SeedAccountRow = { }; export type SeedSummary = { - created: { users: number; households: number; memberships: number }; + created: { + users: number; + households: number; + memberships: number; + categories: number; + accounts: number; + }; accounts: SeedAccountRow[]; }; const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"]; -const DOMAIN_COLLECTIONS = ["households", "householdMembers", "auditLogs"]; +const DOMAIN_COLLECTIONS = [ + "households", + "householdMembers", + "auditLogs", + "categories", + "accounts", +]; type AuthApi = ReturnType["api"]; @@ -73,7 +93,7 @@ export async function runSeed({ const db = getDb(); await connectMongoose(); const summary: SeedSummary = { - created: { users: 0, households: 0, memberships: 0 }, + created: { users: 0, households: 0, memberships: 0, categories: 0, accounts: 0 }, accounts: [], }; @@ -180,7 +200,53 @@ export async function runSeed({ } } - // 5) Geliştirme hesapları tablosu + // 5) Varsayılan kategoriler ve örnek hesaplar + const householdIds = new Map(); + for (const h of SEED_HOUSEHOLDS) { + const doc = await HouseholdModel.findOne({ name: h.name }).select({ _id: 1 }).lean(); + householdIds.set(h.name, doc!._id); + summary.created.categories += await installDefaultCategories(doc!._id, actor); + } + const openingDate = new Date(Date.now() - 183 * 24 * 60 * 60 * 1000); + for (const [order, a] of SEED_ACCOUNTS.entries()) { + const householdId = householdIds.get(a.household)!; + if (await AccountModel.exists({ householdId, name: a.name })) continue; + const payer = a.creditCard + ? await AccountModel.findOne({ householdId, name: a.creditCard.paymentAccount }) + .select({ _id: 1 }) + .lean() + : null; + await AccountModel.create({ + householdId, + name: a.name, + type: a.type, + institution: a.institution ?? "", + mealCardProvider: a.mealCardProvider ?? null, + openingBalanceMinor: a.openingBalanceMinor, + openingDate, + color: a.color, + icon: a.icon, + order, + ownerUserId: a.owner ? new Types.ObjectId(ids.get(a.owner)!) : null, + isPrivate: a.isPrivate ?? false, + overdraft: a.overdraft ? { enabled: true, ...a.overdraft } : null, + creditCard: a.creditCard + ? { + limitMinor: a.creditCard.limitMinor, + statementDay: a.creditCard.statementDay, + dueRule: { kind: "daysAfter", value: a.creditCard.dueDays }, + paymentAccountId: payer?._id ?? null, + } + : null, + createdBy: actor, + updatedBy: actor, + }); + summary.created.accounts++; + } + if (summary.created.categories) log(`✓ Varsayılan kategoriler: ${summary.created.categories}`); + if (summary.created.accounts) log(`✓ Örnek hesaplar: ${summary.created.accounts}`); + + // 6) Geliştirme hesapları tablosu const all = [ { username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const }, ...SEED_USERS,