feat(seed): örnek cariler ve hareketleri

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 11:58:37 +03:00
parent b6c6be5c97
commit 5648eafd7c
3 changed files with 165 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
import type { Types } from "mongoose";
import { createContact, createContactEntry } from "@/features/contacts/service";
import { AccountModel } from "@/models/account";
import { ContactModel } from "@/models/contact";
import type { ContactEntryKind, ContactRelation } from "@/shared/contacts";
import { todayIstanbul } from "@/shared/dates";
import { formatMoneyInput } from "@/shared/money";
import { addDays } from "@/shared/recurrence";
import { contactEntrySchema, contactFormSchema } from "@/shared/schemas/contact";
/**
* Örnek cariler (Faz 8a): vadesi yaklaşan alacak, kısmen ödenen borç, vadesi geçmiş küçük alacak
* ve "yalnızca ben" cari. İdempotent: cari `seedRef` ile; hareketler yalnızca cari ilk
* oluşturulduğunda eklenir.
*/
type SeedContact = {
ref: string;
household: string;
by: string;
name: string;
relation: ContactRelation;
phone?: string;
isPrivate?: boolean;
dueInDays?: number;
reminderDays?: number;
entries: {
kind: ContactEntryKind;
account: string;
amountMinor: number;
daysAgo: number;
description?: string;
}[];
};
const CONTACTS: SeedContact[] = [
{
ref: "ahmet",
household: "Çakır Ailesi",
by: "admin",
name: "Ahmet Yılmaz",
relation: "friend",
phone: "0532 000 00 01",
dueInDays: 5,
reminderDays: 7,
entries: [
{
kind: "lend",
account: "Ziraat Maaş",
amountMinor: 5_000_00,
daysAgo: 70,
description: "Araba tamiri için",
},
{ kind: "collect", account: "Cüzdan", amountMinor: 2_000_00, daysAgo: 30 },
],
},
{
ref: "hasan",
household: "Çakır Ailesi",
by: "admin",
name: "Hasan Çakır",
relation: "family",
dueInDays: 60,
entries: [
{
kind: "borrow",
account: "Ziraat Maaş",
amountMinor: 20_000_00,
daysAgo: 120,
description: "Ev eşyası için",
},
{ kind: "repay", account: "Ziraat Maaş", amountMinor: 5_000_00, daysAgo: 60 },
{ kind: "repay", account: "Ziraat Maaş", amountMinor: 5_000_00, daysAgo: 10 },
],
},
{
ref: "elif",
household: "Çakır Ailesi",
by: "ayse",
name: "Elif (komşu)",
relation: "neighbor",
dueInDays: -2,
entries: [
{
kind: "lend",
account: "Cüzdan",
amountMinor: 300_00,
daysAgo: 12,
description: "Pazar alışverişi",
},
],
},
{
ref: "selin",
household: "Çakır Ailesi",
by: "ayse",
name: "Selin (iş arkadaşı)",
relation: "work",
isPrivate: true,
entries: [{ kind: "lend", account: "Cüzdan", amountMinor: 1_000_00, daysAgo: 8 }],
},
{
ref: "ali",
household: "Annemler",
by: "fatma",
name: "Ali Demir",
relation: "family",
dueInDays: 45,
entries: [{ kind: "borrow", account: "Vakıfbank Emekli", amountMinor: 3_000_00, daysAgo: 25 }],
},
];
export async function seedContacts(
householdIds: Map<string, Types.ObjectId>,
userIds: Map<string, string>,
): Promise<number> {
const today = todayIstanbul();
let created = 0;
for (const c of CONTACTS) {
const householdId = householdIds.get(c.household)!;
const seedRef = `contact:${c.ref}`;
if (await ContactModel.exists({ householdId, seedRef })) continue;
const by = userIds.get(c.by)!;
const oldest = Math.max(...c.entries.map((e) => e.daysAgo));
const r = await createContact(
by,
String(householdId),
contactFormSchema.parse({
name: c.name,
relation: c.relation,
phone: c.phone ?? "",
openingDate: addDays(today, -oldest - 1),
isPrivate: c.isPrivate ?? false,
dueDate: c.dueInDays !== undefined ? addDays(today, c.dueInDays) : null,
reminderDays: c.reminderDays ?? 3,
}),
);
if (!r.ok) throw new Error(`Seed carisi oluşturulamadı (${c.name}): ${r.error}`);
await ContactModel.updateOne({ _id: r.data.id }, { $set: { seedRef } });
for (const e of c.entries) {
const account = await AccountModel.findOne({ householdId, name: e.account }).lean();
const x = await createContactEntry(
by,
String(householdId),
r.data.id,
contactEntrySchema.parse({
kind: e.kind,
accountId: String(account!._id),
amount: formatMoneyInput(e.amountMinor),
date: addDays(today, -e.daysAgo),
description: e.description ?? "",
}),
);
if (!x.ok) throw new Error(`Seed cari hareketi oluşturulamadı (${c.name}): ${x.error}`);
}
created++;
}
return created;
}
+1
View File
@@ -47,6 +47,7 @@ describe("seed", () => {
transactions: 0,
recurringRules: 0,
installmentPlans: 0,
contacts: 0,
});
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
+6
View File
@@ -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 { seedContacts } from "./contacts";
import { seedInstallments } from "./installments";
import { seedRecurring } from "./recurring";
import { seedTransactions } from "./transactions";
@@ -47,6 +48,7 @@ export type SeedSummary = {
transactions: number;
recurringRules: number;
installmentPlans: number;
contacts: number;
};
accounts: SeedAccountRow[];
};
@@ -64,6 +66,7 @@ const DOMAIN_COLLECTIONS = [
"reconciliations",
"recurringRules",
"statements",
"contacts",
];
type AuthApi = ReturnType<typeof getAuth>["api"];
@@ -115,6 +118,7 @@ export async function runSeed({
transactions: 0,
recurringRules: 0,
installmentPlans: 0,
contacts: 0,
},
accounts: [],
};
@@ -289,6 +293,8 @@ export async function runSeed({
);
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 = [