feat(seed): örnek düzenli işlemler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, SeedRule[]> = {
|
||||
"Ç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<string, Types.ObjectId>,
|
||||
userIds: Map<string, string>,
|
||||
): Promise<number> {
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof getAuth>["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 = [
|
||||
|
||||
Reference in New Issue
Block a user