import { Types } from "mongoose"; import { installDefaultStockCategories } from "@/features/stock/service"; import { ProductTypeModel } from "@/models/product-type"; import { StockCategoryModel } from "@/models/stock-category"; import { StockMovementModel } from "@/models/stock-movement"; import { todayIstanbul } from "@/shared/dates"; import { addDays } from "@/shared/recurrence"; import { foldTr } from "@/shared/search"; /** * Örnek ev stoğu (Faz 11). Varsayılan stok kategorileri her haneye; Çakır Ailesi'ne ürün tipleri * ve son 6 ayın haftalık tüketim simülasyonu (alım, tüketim, fire, sayım, SKT). Deterministik; * idempotent: ürün tipi `seedRef` ile bir kez oluşturulur. Miktarlar binde bir. */ type SeedProduct = { ref: string; category: string; name: string; unit: string; target: number; /** Alımda paket miktarı (alım = hedefe tamamlayan paket sayısı × paket). */ pack: number; perDay: number; location: string; step?: number; threshold?: number; shelfDays?: number; /** Her n. alımdan sonra fire (oran %). */ waste?: { every: number; pct: number; reason: "spoiled" | "expired" | "damaged" }; /** Bu günden (gün önce) sonra yeniden alınmaz: bugün azalmış görünür. */ stopRefillDaysAgo?: number; /** Her n haftada bir küçük sayım farkı (−%). */ countDrift?: { everyWeeks: number; pct: number }; }; const K = 1000; export const PRODUCTS: SeedProduct[] = [ // Mutfak › Bakliyat { ref: "kirmizi-mercimek", category: "mutfak.bakliyat", name: "Kırmızı Mercimek", unit: "kg", target: 2 * K, pack: 1 * K, perDay: 40, location: "Kiler", step: 100, stopRefillDaysAgo: 40, }, { ref: "kuru-fasulye", category: "mutfak.bakliyat", name: "Kuru Fasulye", unit: "kg", target: 2 * K, pack: 1 * K, perDay: 25, location: "Kiler", step: 250, }, { ref: "nohut", category: "mutfak.bakliyat", name: "Nohut", unit: "kg", target: 2 * K, pack: 1 * K, perDay: 20, location: "Kiler", step: 250, }, { ref: "pirinc", category: "mutfak.bakliyat", name: "Baldo Pirinç", unit: "kg", target: 5 * K, pack: 2_500, perDay: 90, location: "Kiler", step: 500, }, // Mutfak › Manav { ref: "domates", category: "mutfak.manav", name: "Domates", unit: "kg", target: 3 * K, pack: 1 * K, perDay: 300, location: "Buzdolabı", step: 500, shelfDays: 8, waste: { every: 2, pct: 30, reason: "spoiled" }, }, { ref: "patates", category: "mutfak.manav", name: "Patates", unit: "kg", target: 5 * K, pack: 1 * K, perDay: 250, location: "Kiler", step: 500, waste: { every: 4, pct: 10, reason: "spoiled" }, }, { ref: "sogan", category: "mutfak.manav", name: "Kuru Soğan", unit: "kg", target: 3 * K, pack: 1 * K, perDay: 120, location: "Kiler", step: 500, }, // Mutfak › Şarküteri { ref: "beyaz-peynir", category: "mutfak.sarkuteri", name: "Beyaz Peynir", unit: "kg", target: 1 * K, pack: 500, perDay: 30, location: "Buzdolabı", step: 100, shelfDays: 45, }, { ref: "yumurta", category: "mutfak.sarkuteri", name: "Yumurta", unit: "adet", target: 30 * K, pack: 15 * K, perDay: 3 * K, location: "Buzdolabı", shelfDays: 25, }, { ref: "sut", category: "mutfak.sarkuteri", name: "Süt", unit: "lt", target: 6 * K, pack: 1 * K, perDay: 700, location: "Buzdolabı", shelfDays: 12, waste: { every: 6, pct: 15, reason: "expired" }, }, // Genel Temizlik { ref: "tablet-deterjan", category: "genel-temizlik.bulasik", name: "Tablet Deterjan", unit: "tablet", target: 60 * K, pack: 40 * K, perDay: 720, location: "Lavabo altı", stopRefillDaysAgo: 40, }, { ref: "elde-yikama", category: "genel-temizlik.bulasik", name: "Elde Yıkama Deterjanı", unit: "lt", target: 1_500, pack: 750, perDay: 15, location: "Lavabo altı", step: 100, }, { ref: "toz-deterjan", category: "genel-temizlik.camasir", name: "Toz Deterjan", unit: "yıkama", target: 80 * K, pack: 50 * K, perDay: 700, location: "Banyo dolabı", countDrift: { everyWeeks: 8, pct: 4 }, }, { ref: "yumusatici", category: "genel-temizlik.camasir", name: "Yumuşatıcı", unit: "lt", target: 3 * K, pack: 1_440, perDay: 40, location: "Banyo dolabı", step: 100, }, { ref: "camasir-suyu", category: "genel-temizlik.genel", name: "Çamaşır Suyu", unit: "lt", target: 3 * K, pack: 1 * K, perDay: 25, location: "Balkon", step: 250, }, { ref: "tuvalet-kagidi", category: "genel-temizlik.genel", name: "Tuvalet Kağıdı", unit: "rulo", target: 32 * K, pack: 16 * K, perDay: 600, location: "Banyo dolabı", countDrift: { everyWeeks: 10, pct: 6 }, }, // Kişisel Temizlik { ref: "sampuan", category: "kisisel-temizlik.sampuan", name: "Şampuan", unit: "adet", target: 3 * K, pack: 1 * K, perDay: 50, location: "Banyo dolabı", }, { ref: "tiras-bicagi", category: "kisisel-temizlik.tiras", name: "Tıraş Bıçağı", unit: "adet", target: 8 * K, pack: 4 * K, perDay: 150, location: "Banyo dolabı", }, { ref: "dis-macunu", category: "kisisel-temizlik.kisisel-bakim", name: "Diş Macunu", unit: "adet", target: 4 * K, pack: 2 * K, perDay: 70, location: "Banyo dolabı", stopRefillDaysAgo: 60, }, ]; const START_DAYS_AGO = 182; type Row = { kind: "purchase" | "consume" | "waste" | "count_adjust"; daysAgo: number; quantity: number; expiryDate?: string | null; wasteReason?: string | null; note?: string; initial?: boolean; }; /** Haftalık simülasyon: tüketim, eşikte alım, fire ve sayım farkı. Stok asla eksiye düşmez. */ export function simulate(p: SeedProduct, today: string): Row[] { const rows: Row[] = []; let stock = p.target; let purchases = 0; rows.push({ kind: "count_adjust", daysAgo: START_DAYS_AGO, quantity: stock, initial: true }); let week = 0; for (let d = START_DAYS_AGO - 7; d >= 0; d -= 7) { week++; // Haftalık tüketim azaltma adımının katı (ör. tam tablet, 0,25 kg). const step = p.step ?? K; const consume = Math.min(stock, Math.max(step, Math.round((p.perDay * 7) / step) * step)); if (consume > 0) { rows.push({ kind: "consume", daysAgo: d, quantity: consume }); stock -= consume; } if (p.countDrift && week % p.countDrift.everyWeeks === 0 && stock > 0) { const drift = Math.min(stock, Math.round((p.target * p.countDrift.pct) / 100)); if (drift > 0) { rows.push({ kind: "count_adjust", daysAgo: d, quantity: -drift, note: "Sayım" }); stock -= drift; } } const canRefill = p.stopRefillDaysAgo === undefined || d > p.stopRefillDaysAgo; if (canRefill && stock < (p.target * 35) / 100) { const packs = Math.max(1, Math.ceil((p.target - stock) / p.pack)); const quantity = packs * p.pack; purchases++; rows.push({ kind: "purchase", daysAgo: d, quantity, expiryDate: p.shelfDays ? addDays(addDays(today, -d), p.shelfDays) : null, }); stock += quantity; if (p.waste && purchases % p.waste.every === 0 && d >= 3) { const w = Math.min(stock, Math.round((quantity * p.waste.pct) / 100)); rows.push({ kind: "waste", daysAgo: d - 3, quantity: w, wasteReason: p.waste.reason }); stock -= w; } } } return rows; } export async function seedStock( householdIds: Map, users: Map, ): Promise { const actor = new Types.ObjectId(users.get("ayse")!); for (const [, id] of householdIds) await installDefaultStockCategories(id, actor); const householdId = householdIds.get("Çakır Ailesi")!; const cats = await StockCategoryModel.find({ householdId, defaultKey: { $type: "string" } }) .select({ defaultKey: 1 }) .lean<{ _id: Types.ObjectId; defaultKey: string }[]>(); const catByKey = new Map(cats.map((c) => [c.defaultKey, c._id])); const today = todayIstanbul(); let created = 0; for (const p of PRODUCTS) { if (await ProductTypeModel.exists({ householdId, seedRef: p.ref })) continue; const doc = await ProductTypeModel.create({ householdId, categoryId: catByKey.get(p.category)!, name: p.name, nameKey: foldTr(p.name), unit: p.unit, targetQuantity: p.target, thresholdPct: p.threshold ?? 20, location: p.location, step: p.step ?? K, seedRef: p.ref, createdBy: actor, updatedBy: actor, }); const base = Date.now() - START_DAYS_AGO * 86_400_000; // Ham ekleme: aynı tarihli hareketlerin sırası (createdAt) korunur. await StockMovementModel.collection.insertMany( simulate(p, today).map((r, i) => { const at = new Date(base + i * 1000); return { householdId, productTypeId: doc._id, kind: r.kind, date: addDays(today, -r.daysAgo), quantity: r.quantity, wasteReason: r.wasteReason ?? null, expiryDate: r.expiryDate ?? null, note: r.note ?? "", initial: r.initial ?? false, countId: null, deletedAt: null, seedRef: `${p.ref}:${i}`, createdBy: actor, createdAt: at, updatedAt: at, }; }), ); created++; } return created; }