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,
|
categories: 0,
|
||||||
accounts: 0,
|
accounts: 0,
|
||||||
transactions: 0,
|
transactions: 0,
|
||||||
|
recurringRules: 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);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { seedRecurring } from "./recurring";
|
||||||
import { seedTransactions } from "./transactions";
|
import { seedTransactions } from "./transactions";
|
||||||
import {
|
import {
|
||||||
ADMIN_USERNAME,
|
ADMIN_USERNAME,
|
||||||
@@ -43,6 +44,7 @@ export type SeedSummary = {
|
|||||||
categories: number;
|
categories: number;
|
||||||
accounts: number;
|
accounts: number;
|
||||||
transactions: number;
|
transactions: number;
|
||||||
|
recurringRules: number;
|
||||||
};
|
};
|
||||||
accounts: SeedAccountRow[];
|
accounts: SeedAccountRow[];
|
||||||
};
|
};
|
||||||
@@ -58,6 +60,7 @@ const DOMAIN_COLLECTIONS = [
|
|||||||
"payees",
|
"payees",
|
||||||
"attachments",
|
"attachments",
|
||||||
"reconciliations",
|
"reconciliations",
|
||||||
|
"recurringRules",
|
||||||
];
|
];
|
||||||
|
|
||||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
@@ -107,6 +110,7 @@ export async function runSeed({
|
|||||||
categories: 0,
|
categories: 0,
|
||||||
accounts: 0,
|
accounts: 0,
|
||||||
transactions: 0,
|
transactions: 0,
|
||||||
|
recurringRules: 0,
|
||||||
},
|
},
|
||||||
accounts: [],
|
accounts: [],
|
||||||
};
|
};
|
||||||
@@ -272,6 +276,9 @@ export async function runSeed({
|
|||||||
const userIdMap = new Map([...ids.entries()]);
|
const userIdMap = new Map([...ids.entries()]);
|
||||||
summary.created.transactions = await seedTransactions(householdIds, userIdMap);
|
summary.created.transactions = await seedTransactions(householdIds, userIdMap);
|
||||||
if (summary.created.transactions) log(`✓ Örnek işlemler: ${summary.created.transactions}`);
|
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
|
// 7) Geliştirme hesapları tablosu
|
||||||
const all = [
|
const all = [
|
||||||
|
|||||||
Reference in New Issue
Block a user