feat(seed): varsayılan kategoriler, örnek hesaplar ve 002 migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:57:58 +03:00
parent d2aa9d8ae2
commit 0558d92203
6 changed files with 202 additions and 10 deletions
@@ -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);
}
},
};
+2 -1
View File
@@ -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";
+4 -2
View File
@@ -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) {
+96
View File
@@ -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",
},
];
+10 -2
View File
@@ -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);
});
+71 -5
View File
@@ -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<typeof getAuth>["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<string, Types.ObjectId>();
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,