380c9b3647
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
5.0 KiB
TypeScript
157 lines
5.0 KiB
TypeScript
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;
|
||
}
|