feat(seed): örnek varlıklar ve hareketler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,18 @@ Biçim [Keep a Changelog](https://keepachangelog.com/tr-TR/1.1.0/) esas alınır
|
|||||||
|
|
||||||
## [Yayınlanmamış]
|
## [Yayınlanmamış]
|
||||||
|
|
||||||
|
### Eklendi — Faz 9a: varlık lotları ve kâr/zarar
|
||||||
|
|
||||||
|
- Birikim sayfası (`/birikim`): altın, döviz ve ziynet varlıkları (`assets`); toplam güncel değer, toplam maliyet, gerçekleşmemiş ve gerçekleşmiş kâr/zarar; elde kalmayan varlıklar ayrı grupta. Fiyatı olmayan varlık toplam değere katılmaz ve belirtilir.
|
||||||
|
- Varlık hareketleri (`assetMovements`): alım, gelen hediye/miras, satış, hediye/çıkış; tarih, miktar, tutar veya birim fiyat (ikisi de boşsa fiyat tablosundan: alımda satış, satış ve hediyede alış fiyatı), hesap, saklama yeri, not. Geçmiş tarihli satış/çıkış o tarihteki miktarla sınırlıdır.
|
||||||
|
- Hesap bağlantısı: alım hesaptan varlığa, satış varlıktan hesaba transfer olarak kaydedilir (bakiye değişir, gelir/gider raporlarına girmez). İşlem listesinde varlık adıyla görünür; bağlı işlem tek başına düzenlenemez/silinemez, hareket silinince birlikte silinir.
|
||||||
|
- Hesap motoru (`shared/portfolio.ts`): ağırlıklı ortalama maliyet, satışta ortalama maliyetle gerçekleşmiş K/Z, çıkışta K/Z yazılmaz, gelen hediyenin maliyeti o günkü değer (yatırım sayılmaz); güncel değer alış fiyatıyla (Faz 0), gerçekleşmemiş K/Z TL ve %.
|
||||||
|
- `/birikim/:id`: özet (eldeki miktar, ortalama maliyet, maliyet, güncel değer, K/Z, yatırılan, satış geliri), hareket geçmişi (kalan miktar, satış K/Z'si), hareket silme (sonraki çıkışı eksiye düşürecekse reddedilir).
|
||||||
|
- "Yalnızca ben" varlık: varlık, hareketleri ve bağlı hesap işlemleri yalnızca sahibine görünür.
|
||||||
|
- Seed: aylık alımlı ve bir satışlı gram altın birikimi, düğün ziynetleri (hediye giriş ve çıkış), Emre'nin "yalnızca ben" dolar varlığı, Annemler'de fiyatsız bilezikler.
|
||||||
|
- `009` migration'ı: `assets`, `assetMovements` index'leri, işlemlerde `assetId`. Denetim kaydı: `asset.delete`, `asset.movement.delete`.
|
||||||
|
- Testler: ortalama maliyet, satış/çıkış, hediye, eksi miktar, oran biçimi (birim); alım/satış transferleri ve bakiye, fiyat tablosundan tutar, gerçekleşmiş K/Z, kilitli işlem, hareket silme, gizli varlık, izleyici ve başka hane (entegrasyon).
|
||||||
|
|
||||||
## [0.8.0] - 2026-11-05
|
## [0.8.0] - 2026-11-05
|
||||||
|
|
||||||
Faz 8: cari hesaplar ve aynî borç/alacak (8a ve 8b alt fazları).
|
Faz 8: cari hesaplar ve aynî borç/alacak (8a ve 8b alt fazları).
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ describe("seed", () => {
|
|||||||
expect(summary.created.categories).toBeGreaterThan(40);
|
expect(summary.created.categories).toBeGreaterThan(40);
|
||||||
expect(summary.created.accounts).toBe(7);
|
expect(summary.created.accounts).toBe(7);
|
||||||
expect(summary.created.transactions).toBeGreaterThan(150);
|
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 getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1);
|
||||||
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
|
expect(await HouseholdMemberModel.countDocuments()).toBe(memberCount);
|
||||||
});
|
});
|
||||||
@@ -49,6 +50,7 @@ describe("seed", () => {
|
|||||||
installmentPlans: 0,
|
installmentPlans: 0,
|
||||||
contacts: 0,
|
contacts: 0,
|
||||||
inKind: 0,
|
inKind: 0,
|
||||||
|
assets: 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);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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 { seedContacts } from "./contacts";
|
import { seedContacts } from "./contacts";
|
||||||
|
import { seedAssets } from "./assets";
|
||||||
import { seedInKind } from "./in-kind";
|
import { seedInKind } from "./in-kind";
|
||||||
import { seedInstallments } from "./installments";
|
import { seedInstallments } from "./installments";
|
||||||
import { seedRecurring } from "./recurring";
|
import { seedRecurring } from "./recurring";
|
||||||
@@ -51,6 +52,7 @@ export type SeedSummary = {
|
|||||||
installmentPlans: number;
|
installmentPlans: number;
|
||||||
contacts: number;
|
contacts: number;
|
||||||
inKind: number;
|
inKind: number;
|
||||||
|
assets: number;
|
||||||
};
|
};
|
||||||
accounts: SeedAccountRow[];
|
accounts: SeedAccountRow[];
|
||||||
};
|
};
|
||||||
@@ -71,6 +73,8 @@ const DOMAIN_COLLECTIONS = [
|
|||||||
"contacts",
|
"contacts",
|
||||||
"assetPrices",
|
"assetPrices",
|
||||||
"inKindDebts",
|
"inKindDebts",
|
||||||
|
"assets",
|
||||||
|
"assetMovements",
|
||||||
];
|
];
|
||||||
|
|
||||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
@@ -124,6 +128,7 @@ export async function runSeed({
|
|||||||
installmentPlans: 0,
|
installmentPlans: 0,
|
||||||
contacts: 0,
|
contacts: 0,
|
||||||
inKind: 0,
|
inKind: 0,
|
||||||
|
assets: 0,
|
||||||
},
|
},
|
||||||
accounts: [],
|
accounts: [],
|
||||||
};
|
};
|
||||||
@@ -302,6 +307,8 @@ export async function runSeed({
|
|||||||
if (summary.created.contacts) log(`✓ Örnek cariler: ${summary.created.contacts}`);
|
if (summary.created.contacts) log(`✓ Örnek cariler: ${summary.created.contacts}`);
|
||||||
summary.created.inKind = await seedInKind(householdIds, userIdMap);
|
summary.created.inKind = await seedInKind(householdIds, userIdMap);
|
||||||
if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`);
|
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
|
// 7) Geliştirme hesapları tablosu
|
||||||
const all = [
|
const all = [
|
||||||
|
|||||||
Reference in New Issue
Block a user