feat(seed): örnek varlıklar ve hareketler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 23:17:44 +03:00
parent a818c110cf
commit b8306b3b5d
4 changed files with 183 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import type { Types } from "mongoose";
import { addMovement, createAsset } from "@/features/assets/service";
import { AccountModel } from "@/models/account";
import { AssetModel } from "@/models/asset";
import { AssetMovementModel } from "@/models/asset-movement";
import { TransactionModel } from "@/models/transaction";
import type { AssetKey } from "@/shared/assets";
import { todayIstanbul } from "@/shared/dates";
import type { MovementKind } from "@/shared/portfolio";
import { addDays } from "@/shared/recurrence";
import { assetFormSchema, movementFormSchema } from "@/shared/schemas/asset";
/**
* Örnek varlıklar (Faz 9a). Tutarı boş alımlar fiyat tablosundaki örnek fiyatlardan (seedInKind
* önce çalışır) hesaplanır. İdempotent: varlık `seedRef` ile bir kez oluşturulur.
*/
type SeedMovement = {
kind: MovementKind;
daysAgo: number;
quantity: string;
amount?: string;
account?: string;
storage?: string;
note?: string;
};
type SeedAsset = {
ref: string;
household: string;
by: string;
assetKey: AssetKey;
name: string;
isPrivate?: boolean;
note?: string;
movements: SeedMovement[];
};
const monthly = (from: number, to: number): SeedMovement[] =>
Array.from({ length: to - from + 1 }, (_, i) => ({
kind: "buy" as const,
daysAgo: (to - i) * 30 + 2,
quantity: "1",
account: "Ziraat Maaş",
storage: "Ev kasası",
}));
const ITEMS: SeedAsset[] = [
{
ref: "gram-altin",
household: "Çakır Ailesi",
by: "ayse",
assetKey: "gram_gold",
name: "Gram altın birikimi",
note: "Her ay maaştan 1 gr",
movements: [
{
kind: "buy",
daysAgo: 178,
quantity: "4",
account: "Birikim (Vadeli)",
storage: "Ev kasası",
note: "İlk alım",
},
...monthly(1, 5),
{ kind: "sell", daysAgo: 12, quantity: "2", account: "Birikim (Vadeli)", note: "Tatil için" },
],
},
{
ref: "ziynet",
household: "Çakır Ailesi",
by: "admin",
assetKey: "quarter_gold",
name: "Düğün ziynetleri",
movements: [
{
kind: "gift_in",
daysAgo: 175,
quantity: "6",
storage: "Banka kiralık kasa",
note: "Düğün takıları",
},
{ kind: "gift_out", daysAgo: 40, quantity: "1", note: "Yeğenin düğünü" },
],
},
{
ref: "emre-dolar",
household: "Çakır Ailesi",
by: "emre",
assetKey: "usd",
name: "Dolar",
isPrivate: true,
movements: [
{ kind: "buy", daysAgo: 120, quantity: "200", account: "Ziraat Maaş", storage: "Cüzdan" },
],
},
{
ref: "bilezik",
household: "Annemler",
by: "fatma",
assetKey: "bracelet_22k",
name: "Bilezikler",
note: "Fiyat tablosunda bilezik fiyatı yok (örnek: fiyatsız varlık)",
movements: [{ kind: "gift_in", daysAgo: 160, quantity: "40", amount: "120.000" }],
},
];
export async function seedAssets(
householdIds: Map<string, Types.ObjectId>,
userIds: Map<string, string>,
): Promise<number> {
const today = todayIstanbul();
let created = 0;
for (const x of ITEMS) {
const householdId = householdIds.get(x.household)!;
const seedRef = `asset:${x.ref}`;
if (await AssetModel.exists({ householdId, seedRef })) continue;
const by = userIds.get(x.by)!;
const hh = String(householdId);
const r = await createAsset(
by,
hh,
assetFormSchema.parse({
assetKey: x.assetKey,
name: x.name,
note: x.note ?? "",
isPrivate: x.isPrivate ?? false,
}),
);
if (!r.ok) throw new Error(`Seed varlığı oluşturulamadı (${x.ref}): ${r.error}`);
await AssetModel.updateOne({ _id: r.data.id }, { $set: { seedRef } });
for (const [i, m] of x.movements.entries()) {
const account = m.account
? String((await AccountModel.findOne({ householdId, name: m.account }).lean())!._id)
: "";
const res = await addMovement(
by,
hh,
r.data.id,
movementFormSchema.parse({
kind: m.kind,
date: addDays(today, -m.daysAgo),
quantity: m.quantity,
amount: m.amount ?? "",
accountId: account,
storage: m.storage ?? "",
note: m.note ?? "",
}),
);
if (!res.ok) throw new Error(`Seed varlık hareketi eklenemedi (${x.ref}): ${res.error}`);
const ref = `${seedRef}:${i}`;
const mv = await AssetMovementModel.findOneAndUpdate(
{ _id: res.data.id },
{ $set: { seedRef: ref } },
).lean();
if (mv?.transactionId)
await TransactionModel.updateOne({ _id: mv.transactionId }, { $set: { seedRef: ref } });
}
created++;
}
return created;
}
+2
View File
@@ -32,6 +32,7 @@ describe("seed", () => {
expect(summary.created.categories).toBeGreaterThan(40);
expect(summary.created.accounts).toBe(7);
expect(summary.created.transactions).toBeGreaterThan(150);
expect(summary.created.assets).toBe(4);
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
});
@@ -49,6 +50,7 @@ describe("seed", () => {
installmentPlans: 0,
contacts: 0,
inKind: 0,
assets: 0,
});
expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length);
+7
View File
@@ -11,6 +11,7 @@ import { HouseholdMemberModel } from "@/models/household-member";
import { storedEmail } from "@/shared/account-email";
import { runMigrations } from "../migrations";
import { seedContacts } from "./contacts";
import { seedAssets } from "./assets";
import { seedInKind } from "./in-kind";
import { seedInstallments } from "./installments";
import { seedRecurring } from "./recurring";
@@ -51,6 +52,7 @@ export type SeedSummary = {
installmentPlans: number;
contacts: number;
inKind: number;
assets: number;
};
accounts: SeedAccountRow[];
};
@@ -71,6 +73,8 @@ const DOMAIN_COLLECTIONS = [
"contacts",
"assetPrices",
"inKindDebts",
"assets",
"assetMovements",
];
type AuthApi = ReturnType<typeof getAuth>["api"];
@@ -124,6 +128,7 @@ export async function runSeed({
installmentPlans: 0,
contacts: 0,
inKind: 0,
assets: 0,
},
accounts: [],
};
@@ -302,6 +307,8 @@ export async function runSeed({
if (summary.created.contacts) log(`✓ Örnek cariler: ${summary.created.contacts}`);
summary.created.inKind = await seedInKind(householdIds, userIdMap);
if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`);
summary.created.assets = await seedAssets(householdIds, userIdMap);
if (summary.created.assets) log(`✓ Örnek varlıklar: ${summary.created.assets}`);
// 7) Geliştirme hesapları tablosu
const all = [