From 380c9b364799d82b9dbcb1bd8820403c02601ac2 Mon Sep 17 00:00:00 2001 From: suphicakir Date: Thu, 5 Nov 2026 12:37:48 +0300 Subject: [PATCH] =?UTF-8?q?feat(seed):=20=C3=B6rnek=20fiyat=20ge=C3=A7mi?= =?UTF-8?q?=C5=9Fi=20ve=20ayn=C3=AE=20kay=C4=B1tlar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- scripts/seed/in-kind.ts | 156 +++++++++++++++++++++++++++++++++++ scripts/seed/run.int.test.ts | 1 + scripts/seed/run.ts | 7 ++ 3 files changed, 164 insertions(+) create mode 100644 scripts/seed/in-kind.ts diff --git a/scripts/seed/in-kind.ts b/scripts/seed/in-kind.ts new file mode 100644 index 0000000..0f3ccc2 --- /dev/null +++ b/scripts/seed/in-kind.ts @@ -0,0 +1,156 @@ +import type { Types } from "mongoose"; +import { addContribution, createInKind, savePrice } from "@/features/in-kind/service"; +import { AccountModel } from "@/models/account"; +import { AssetPriceModel } from "@/models/asset-price"; +import { ContactModel } from "@/models/contact"; +import { InKindDebtModel } from "@/models/in-kind-debt"; +import type { AssetKey } from "@/shared/assets"; +import { todayIstanbul } from "@/shared/dates"; +import { formatMoneyInput } from "@/shared/money"; +import { addDays, addMonths } from "@/shared/recurrence"; +import { + assetPriceFormSchema, + contributionSchema, + inKindFormSchema, +} from "@/shared/schemas/in-kind"; + +/** + * Örnek fiyat tablosu ve aynî kayıtlar (Faz 8b). Fiyatlar örnek amaçlıdır (gerçek piyasa verisi + * değildir): son 6 ay, ayda bir kayıt, doğrusal artış. İdempotent. + */ + +/** [6 ay önceki alış, bugünkü alış, satış farkı] — birim başına kuruş. */ +const PRICE_PATH: Partial> = { + gram_gold: [4_900_00, 5_600_00, 100_00], + quarter_gold: [8_050_00, 9_200_00, 250_00], + usd: [40_20, 42_80, 30], + eur: [46_90, 49_50, 40], +}; + +async function seedPrices(householdId: Types.ObjectId, actorId: string, today: string) { + for (const [k, [from, to, spread]] of Object.entries(PRICE_PATH)) { + const key = k as AssetKey; + for (let i = 6; i >= 0; i--) { + const date = i === 0 ? today : addMonths(today, -i); + if (await AssetPriceModel.exists({ householdId, assetKey: key, date })) continue; + const buy = Math.round(to - ((to - from) * i) / 6); + await savePrice( + actorId, + String(householdId), + assetPriceFormSchema.parse({ + assetKey: key, + date, + buy: formatMoneyInput(buy), + sell: formatMoneyInput(buy + spread), + }), + ); + } + } +} + +type SeedInKind = { + ref: string; + household: string; + contact: string; + by: string; + direction: "debt" | "receivable"; + assetKey: AssetKey; + quantity: string; + daysAgo: number; + note?: string; + savings?: string; + monthlyPlanMinor?: number; + contributions?: { from: string; amountMinor: number; daysAgo: number }[]; +}; + +const ITEMS: SeedInKind[] = [ + { + ref: "hasan-altin", + household: "Çakır Ailesi", + contact: "contact:hasan", + by: "admin", + direction: "debt", + assetKey: "gram_gold", + quantity: "5", + daysAgo: 95, + note: "Düğün için ödünç alınan altın", + savings: "Birikim (Vadeli)", + monthlyPlanMinor: 4_000_00, + contributions: [ + { from: "Ziraat Maaş", amountMinor: 4_000_00, daysAgo: 60 }, + { from: "Ziraat Maaş", amountMinor: 4_000_00, daysAgo: 30 }, + ], + }, + { + ref: "elif-usd", + household: "Çakır Ailesi", + contact: "contact:elif", + by: "ayse", + direction: "receivable", + assetKey: "usd", + quantity: "100", + daysAgo: 40, + }, + { + ref: "ali-ceyrek", + household: "Annemler", + contact: "contact:ali", + by: "fatma", + direction: "receivable", + assetKey: "quarter_gold", + quantity: "2", + daysAgo: 150, + note: "Torun doğumunda verilen çeyrekler", + }, +]; + +export async function seedInKind( + householdIds: Map, + userIds: Map, +): Promise { + const today = todayIstanbul(); + await seedPrices(householdIds.get("Çakır Ailesi")!, userIds.get("admin")!, today); + await seedPrices(householdIds.get("Annemler")!, userIds.get("fatma")!, today); + let created = 0; + for (const x of ITEMS) { + const householdId = householdIds.get(x.household)!; + const seedRef = `inkind:${x.ref}`; + if (await InKindDebtModel.exists({ householdId, seedRef })) continue; + const contact = await ContactModel.findOne({ householdId, seedRef: x.contact }).lean(); + if (!contact) continue; + const account = async (name: string) => + String((await AccountModel.findOne({ householdId, name }).lean())!._id); + const by = userIds.get(x.by)!; + const r = await createInKind( + by, + String(householdId), + String(contact._id), + inKindFormSchema.parse({ + direction: x.direction, + assetKey: x.assetKey, + quantity: x.quantity, + date: addDays(today, -x.daysAgo), + note: x.note ?? "", + savingsAccountId: x.savings ? await account(x.savings) : "", + monthlyPlan: x.monthlyPlanMinor ? formatMoneyInput(x.monthlyPlanMinor) : "", + }), + ); + if (!r.ok) throw new Error(`Seed aynî kaydı oluşturulamadı (${x.ref}): ${r.error}`); + await InKindDebtModel.updateOne({ _id: r.data.id }, { $set: { seedRef } }); + for (const c of x.contributions ?? []) { + const res = await addContribution( + by, + String(householdId), + r.data.id, + contributionSchema.parse({ + fromAccountId: await account(c.from), + amount: formatMoneyInput(c.amountMinor), + date: addDays(today, -c.daysAgo), + }), + ); + if (!res.ok) throw new Error(`Seed birikimi oluşturulamadı (${x.ref}): ${res.error}`); + } + created++; + } + return created; +} diff --git a/scripts/seed/run.int.test.ts b/scripts/seed/run.int.test.ts index 6ca440a..d2f5d5c 100644 --- a/scripts/seed/run.int.test.ts +++ b/scripts/seed/run.int.test.ts @@ -48,6 +48,7 @@ describe("seed", () => { recurringRules: 0, installmentPlans: 0, contacts: 0, + inKind: 0, }); expect(await getDb().collection("user").countDocuments()).toBe(SEED_USERS.length + 1); expect(await HouseholdModel.countDocuments()).toBe(SEED_HOUSEHOLDS.length); diff --git a/scripts/seed/run.ts b/scripts/seed/run.ts index 11b2f40..cb7d2ae 100644 --- a/scripts/seed/run.ts +++ b/scripts/seed/run.ts @@ -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 { seedInKind } from "./in-kind"; import { seedInstallments } from "./installments"; import { seedRecurring } from "./recurring"; import { seedTransactions } from "./transactions"; @@ -49,6 +50,7 @@ export type SeedSummary = { recurringRules: number; installmentPlans: number; contacts: number; + inKind: number; }; accounts: SeedAccountRow[]; }; @@ -67,6 +69,8 @@ const DOMAIN_COLLECTIONS = [ "recurringRules", "statements", "contacts", + "assetPrices", + "inKindDebts", ]; type AuthApi = ReturnType["api"]; @@ -119,6 +123,7 @@ export async function runSeed({ recurringRules: 0, installmentPlans: 0, contacts: 0, + inKind: 0, }, accounts: [], }; @@ -295,6 +300,8 @@ export async function runSeed({ 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}`); + summary.created.inKind = await seedInKind(householdIds, userIdMap); + if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`); // 7) Geliştirme hesapları tablosu const all = [