import mongoose, { Types } from "mongoose"; import { getAuth } from "@/lib/auth"; import { getDb } from "@/lib/db/mongo"; import { connectMongoose } from "@/lib/db/mongoose"; import { env } from "@/lib/env"; import { getStorage } from "@/lib/storage"; 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 { seedContacts } from "./contacts"; import { seedInstallments } from "./installments"; import { seedRecurring } from "./recurring"; import { seedTransactions } from "./transactions"; 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); * tekrar çalıştırınca kopya oluşturmaz (kullanıcı adı / hane adı / üyelik çiftine göre). * Production'da yalnızca admin oluşturulur; örnek kullanıcı ve hane oluşturulmaz. */ export type SeedOptions = { reset?: boolean; log?: (line: string) => void }; export type SeedAccountRow = { rol: string; kullaniciAdi: string; eposta: string; durum: string; haneler: string; }; export type SeedSummary = { created: { users: number; households: number; memberships: number; categories: number; accounts: number; transactions: number; recurringRules: number; installmentPlans: number; contacts: number; }; accounts: SeedAccountRow[]; }; const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"]; const DOMAIN_COLLECTIONS = [ "households", "householdMembers", "auditLogs", "categories", "accounts", "transactions", "payees", "attachments", "reconciliations", "recurringRules", "statements", "contacts", ]; type AuthApi = ReturnType["api"]; async function findUserIdByUsername(username: string): Promise { const doc = await getDb() .collection("user") .findOne({ username }, { projection: { _id: 1 } }); return doc ? String(doc._id) : null; } /** Admin olarak giriş yapıp oturum başlıklarını döner (ban gibi oturum isteyen admin API'leri için). */ async function adminHeaders(api: AuthApi, password: string): Promise { const { headers } = await api.signInUsername({ body: { username: ADMIN_USERNAME, password }, returnHeaders: true, }); const cookie = headers .getSetCookie() .map((c) => c.split(";")[0]) .join("; "); return new Headers({ cookie }); } export async function runSeed({ reset = false, log = console.log, }: SeedOptions = {}): Promise { const e = env(); const isProd = e.NODE_ENV === "production"; if (reset && isProd) throw new Error("seed:reset production ortamında çalıştırılamaz."); if (!e.SEED_ADMIN_EMAIL || !e.SEED_ADMIN_PASSWORD) { throw new Error("SEED_ADMIN_EMAIL ve SEED_ADMIN_PASSWORD tanımlı olmalı."); } if (!isProd && !e.SEED_USER_PASSWORD) { throw new Error("SEED_USER_PASSWORD tanımlı olmalı (örnek kullanıcıların ortak şifresi)."); } const api = getAuth().api; const db = getDb(); await connectMongoose(); const summary: SeedSummary = { created: { users: 0, households: 0, memberships: 0, categories: 0, accounts: 0, transactions: 0, recurringRules: 0, installmentPlans: 0, contacts: 0, }, accounts: [], }; if (reset) { // Ek dosyaların diskteki kopyaları da silinsin (yetim dosya kalmasın). const storage = getStorage(); for await (const a of db .collection("attachments") .find({}, { projection: { storageKey: 1, thumbKey: 1 } })) { await storage.delete(String(a.storageKey)); if (a.thumbKey) await storage.delete(String(a.thumbKey)); } for (const name of [...AUTH_COLLECTIONS, ...DOMAIN_COLLECTIONS]) { await db.collection(name).deleteMany({}); } log("• Kimlik doğrulama ve hane koleksiyonları temizlendi."); } // Index'ler ve şema değişiklikleri (production'da Mongoose autoIndex kapalı). await runMigrations(log); // 1) Admin const adminEmail = e.SEED_ADMIN_EMAIL.toLowerCase(); let adminId = await findUserIdByUsername(ADMIN_USERNAME); if (!adminId) { const res = await api.createUser({ body: { email: adminEmail, password: e.SEED_ADMIN_PASSWORD, name: "Yönetici", role: "admin", data: { username: ADMIN_USERNAME }, }, }); adminId = res.user.id; summary.created.users++; log(`✓ Admin oluşturuldu: ${adminEmail} (kullanıcı adı: ${ADMIN_USERNAME})`); } else { log(`• Admin zaten var: ${ADMIN_USERNAME}`); } if (isProd) { log("• Production: örnek kullanıcı ve hane oluşturulmadı."); return summary; } // 2) Örnek kullanıcılar const ids = new Map([[ADMIN_USERNAME, adminId]]); const toBan: SeedUser[] = []; for (const u of SEED_USERS) { let id = await findUserIdByUsername(u.username); if (!id) { const res = await api.createUser({ body: { email: storedEmail(u.username, u.email), password: e.SEED_USER_PASSWORD!, name: u.name, role: u.role, data: { username: u.username }, }, }); id = res.user.id; summary.created.users++; log(`✓ Kullanıcı: ${u.name} (@${u.username})`); } ids.set(u.username, id); if (u.banReason) toBan.push(u); } // 3) Askıdaki kullanıcı(lar) — admin API'si oturum ister. const pendingBans: { user: SeedUser; id: string }[] = []; for (const u of toBan) { const id = ids.get(u.username)!; const doc = await db .collection("user") .findOne({ _id: new Types.ObjectId(id) }, { projection: { banned: 1 } }); if (!doc?.banned) pendingBans.push({ user: u, id }); } if (pendingBans.length) { const headers = await adminHeaders(api, e.SEED_ADMIN_PASSWORD); for (const { user, id } of pendingBans) { await api.banUser({ headers, body: { userId: id, banReason: user.banReason } }); log(`✓ Askıya alındı: @${user.username}`); } } // 4) Haneler ve üyelikler const memberships = new Map(); const actor = new Types.ObjectId(adminId); for (const h of SEED_HOUSEHOLDS) { let household = await HouseholdModel.findOne({ name: h.name }); if (!household) { household = await HouseholdModel.create({ name: h.name, createdBy: actor, updatedBy: actor }); summary.created.households++; log(`✓ Hane: ${h.name}`); } for (const m of h.members) { const userId = new Types.ObjectId(ids.get(m.username)!); const result = await HouseholdMemberModel.updateOne( { householdId: household._id, userId }, { $setOnInsert: { householdId: household._id, userId, createdBy: actor }, $set: { role: m.role, updatedBy: actor }, }, { upsert: true }, ); if (result.upsertedCount) summary.created.memberships++; memberships.set(m.username, [ ...(memberships.get(m.username) ?? []), `${h.name} (${m.role})`, ]); } } // 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) Son 6 ayın örnek işlemleri (faker.seed(42), idempotent) 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}`); summary.created.installmentPlans = await seedInstallments( householdIds.get("Çakır Ailesi")!, userIdMap, ); if (summary.created.installmentPlans) log(`✓ Örnek taksitli alışverişler: ${summary.created.installmentPlans}`); summary.created.contacts = await seedContacts(householdIds, userIdMap); if (summary.created.contacts) log(`✓ Örnek cariler: ${summary.created.contacts}`); // 7) Geliştirme hesapları tablosu const all = [ { username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const }, ...SEED_USERS, ]; summary.accounts = all.map((u) => ({ rol: u.role, kullaniciAdi: u.username, eposta: u.email ?? "—", durum: "banReason" in u && u.banReason ? "askıda" : "aktif", haneler: (memberships.get(u.username) ?? ["—"]).join(", "), })); return summary; } /** Seed sonrası bağlantıları kapatır (CLI ve testler). */ export async function closeSeedConnections() { await mongoose.disconnect(); }