feat(seed): örnek fiyat geçmişi ve aynî kayıtlar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Record<AssetKey, [number, number, number]>> = {
|
||||||
|
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<string, Types.ObjectId>,
|
||||||
|
userIds: Map<string, string>,
|
||||||
|
): Promise<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ describe("seed", () => {
|
|||||||
recurringRules: 0,
|
recurringRules: 0,
|
||||||
installmentPlans: 0,
|
installmentPlans: 0,
|
||||||
contacts: 0,
|
contacts: 0,
|
||||||
|
inKind: 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 { seedInKind } from "./in-kind";
|
||||||
import { seedInstallments } from "./installments";
|
import { seedInstallments } from "./installments";
|
||||||
import { seedRecurring } from "./recurring";
|
import { seedRecurring } from "./recurring";
|
||||||
import { seedTransactions } from "./transactions";
|
import { seedTransactions } from "./transactions";
|
||||||
@@ -49,6 +50,7 @@ export type SeedSummary = {
|
|||||||
recurringRules: number;
|
recurringRules: number;
|
||||||
installmentPlans: number;
|
installmentPlans: number;
|
||||||
contacts: number;
|
contacts: number;
|
||||||
|
inKind: number;
|
||||||
};
|
};
|
||||||
accounts: SeedAccountRow[];
|
accounts: SeedAccountRow[];
|
||||||
};
|
};
|
||||||
@@ -67,6 +69,8 @@ const DOMAIN_COLLECTIONS = [
|
|||||||
"recurringRules",
|
"recurringRules",
|
||||||
"statements",
|
"statements",
|
||||||
"contacts",
|
"contacts",
|
||||||
|
"assetPrices",
|
||||||
|
"inKindDebts",
|
||||||
];
|
];
|
||||||
|
|
||||||
type AuthApi = ReturnType<typeof getAuth>["api"];
|
type AuthApi = ReturnType<typeof getAuth>["api"];
|
||||||
@@ -119,6 +123,7 @@ export async function runSeed({
|
|||||||
recurringRules: 0,
|
recurringRules: 0,
|
||||||
installmentPlans: 0,
|
installmentPlans: 0,
|
||||||
contacts: 0,
|
contacts: 0,
|
||||||
|
inKind: 0,
|
||||||
},
|
},
|
||||||
accounts: [],
|
accounts: [],
|
||||||
};
|
};
|
||||||
@@ -295,6 +300,8 @@ export async function runSeed({
|
|||||||
log(`✓ Örnek taksitli alışverişler: ${summary.created.installmentPlans}`);
|
log(`✓ Örnek taksitli alışverişler: ${summary.created.installmentPlans}`);
|
||||||
summary.created.contacts = await seedContacts(householdIds, userIdMap);
|
summary.created.contacts = await seedContacts(householdIds, userIdMap);
|
||||||
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);
|
||||||
|
if (summary.created.inKind) log(`✓ Örnek aynî kayıtlar ve fiyatlar: ${summary.created.inKind}`);
|
||||||
|
|
||||||
// 7) Geliştirme hesapları tablosu
|
// 7) Geliştirme hesapları tablosu
|
||||||
const all = [
|
const all = [
|
||||||
|
|||||||
Reference in New Issue
Block a user