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:
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { getDb } from "@/lib/db/mongo";
|
import { getDb } from "@/lib/db/mongo";
|
||||||
import { connectMongoose } from "@/lib/db/mongoose";
|
import { connectMongoose } from "@/lib/db/mongoose";
|
||||||
import { migration as m001 } from "./001-initial-indexes";
|
import { migration as m001 } from "./001-initial-indexes";
|
||||||
|
import { migration as m002 } from "./002-accounts-categories";
|
||||||
import type { Migration } from "./types";
|
import type { Migration } from "./types";
|
||||||
|
|
||||||
/** Sıralı migration listesi. Yeni migration: dosyayı ekle ve buraya sırayla ekle. */
|
/** 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";
|
const COLLECTION = "migrations";
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -11,8 +11,10 @@ import { closeSeedConnections, runSeed } from "./seed/run";
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const summary = await runSeed({ reset: process.argv.includes("--reset") });
|
const summary = await runSeed({ reset: process.argv.includes("--reset") });
|
||||||
const { users, households, memberships } = summary.created;
|
const { users, households, memberships, categories, accounts } = summary.created;
|
||||||
console.log(`\nOluşturulan: ${users} kullanıcı, ${households} hane, ${memberships} üyelik.`);
|
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.
|
// Hesap tablosu yalnızca geliştirme ortamında gösterilir.
|
||||||
if (env().NODE_ENV !== "production" && summary.accounts.length) {
|
if (env().NODE_ENV !== "production" && summary.accounts.length) {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { HouseholdRole } from "@/features/households/types";
|
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";
|
import type { SystemRole } from "@/shared/schemas/common";
|
||||||
|
|
||||||
/** Örnek (geliştirme) kullanıcıları. E-postası olmayanlar kullanıcı adıyla giriş yapar. */
|
/** Ö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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -24,18 +24,26 @@ describe("seed", () => {
|
|||||||
it("ilk çalıştırmada admin, örnek kullanıcılar, haneler ve üyelikleri oluşturur", async () => {
|
it("ilk çalıştırmada admin, örnek kullanıcılar, haneler ve üyelikleri oluşturur", async () => {
|
||||||
const summary = await runSeed({ log: silent });
|
const summary = await runSeed({ log: silent });
|
||||||
const memberCount = SEED_HOUSEHOLDS.reduce((n, h) => n + h.members.length, 0);
|
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,
|
users: SEED_USERS.length + 1,
|
||||||
households: SEED_HOUSEHOLDS.length,
|
households: SEED_HOUSEHOLDS.length,
|
||||||
memberships: memberCount,
|
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 getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||||
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
|
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("idempotenttir: ikinci çalıştırma kopya oluşturmaz", async () => {
|
it("idempotenttir: ikinci çalıştırma kopya oluşturmaz", async () => {
|
||||||
const summary = await runSeed({ log: silent });
|
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 getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||||
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
|
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
|
||||||
});
|
});
|
||||||
|
|||||||
+71
-5
@@ -3,11 +3,19 @@ import { getAuth } from "@/lib/auth";
|
|||||||
import { getDb } from "@/lib/db/mongo";
|
import { getDb } from "@/lib/db/mongo";
|
||||||
import { connectMongoose } from "@/lib/db/mongoose";
|
import { connectMongoose } from "@/lib/db/mongoose";
|
||||||
import { env } from "@/lib/env";
|
import { env } from "@/lib/env";
|
||||||
|
import { installDefaultCategories } from "@/features/categories/service";
|
||||||
|
import { AccountModel } from "@/models/account";
|
||||||
import { HouseholdModel } from "@/models/household";
|
import { HouseholdModel } from "@/models/household";
|
||||||
import { HouseholdMemberModel } from "@/models/household-member";
|
import { HouseholdMemberModel } from "@/models/household-member";
|
||||||
import { storedEmail } from "@/shared/account-email";
|
import { storedEmail } from "@/shared/account-email";
|
||||||
import { runMigrations } from "../migrations";
|
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);
|
* İ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 = {
|
export type SeedSummary = {
|
||||||
created: { users: number; households: number; memberships: number };
|
created: {
|
||||||
|
users: number;
|
||||||
|
households: number;
|
||||||
|
memberships: number;
|
||||||
|
categories: number;
|
||||||
|
accounts: number;
|
||||||
|
};
|
||||||
accounts: SeedAccountRow[];
|
accounts: SeedAccountRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const AUTH_COLLECTIONS = ["user", "account", "session", "verification", "rateLimit"];
|
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"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
|
|
||||||
@@ -73,7 +93,7 @@ export async function runSeed({
|
|||||||
const db = getDb();
|
const db = getDb();
|
||||||
await connectMongoose();
|
await connectMongoose();
|
||||||
const summary: SeedSummary = {
|
const summary: SeedSummary = {
|
||||||
created: { users: 0, households: 0, memberships: 0 },
|
created: { users: 0, households: 0, memberships: 0, categories: 0, accounts: 0 },
|
||||||
accounts: [],
|
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 = [
|
const all = [
|
||||||
{ username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const },
|
{ username: ADMIN_USERNAME, name: "Yönetici", email: adminEmail, role: "admin" as const },
|
||||||
...SEED_USERS,
|
...SEED_USERS,
|
||||||
|
|||||||
Reference in New Issue
Block a user