import { Types } from "mongoose"; import { installDefaultStockCategories } from "@/features/stock/service"; import { ProductTypeModel } from "@/models/product-type"; import { StockCategoryModel } from "@/models/stock-category"; import { StockCountModel } from "@/models/stock-count"; 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, periyodik sayım, SKT). * Sayım: Mutfak 15 günde bir (son sayım 21 gün önce → gecikmiş), Genel Temizlik 30 günde bir * (son sayım 14 gün önce). Deterministik; idempotent: ürün tipi `seedRef` ile bir kez * oluşturulur, sayım oturumları `seedRef` ile paylaşılır. 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; /** Ağır/hacimli (akıllı öneride taşıma cezası). */ bulky?: boolean; /** Her n. sayımda küçük eksik (−%, hedefin yüzdesi). */ countDrift?: { everyCounts: 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", bulky: true, category: "genel-temizlik.camasir", name: "Toz Deterjan", unit: "yıkama", target: 80 * K, pack: 50 * K, perDay: 700, location: "Banyo dolabı", countDrift: { everyCounts: 2, pct: 4 }, }, { ref: "yumusatici", bulky: true, 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", bulky: true, 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", bulky: true, category: "genel-temizlik.genel", name: "Tuvalet Kağıdı", unit: "rulo", target: 32 * K, pack: 16 * K, perDay: 600, location: "Banyo dolabı", countDrift: { everyCounts: 3, 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; /** Sayım sıklığı (gün) ve sayım günü kuralı (gün önce), ana stok kategorisine göre. */ const COUNT_PLANS: Record boolean }> = { mutfak: { everyDays: 15, on: (d) => d >= 21 && d % 14 === 7 }, "genel-temizlik": { everyDays: 30, on: (d) => d % 28 === 14 }, }; type Row = { kind: "purchase" | "consume" | "waste" | "count_adjust"; daysAgo: number; quantity: number; expiryDate?: string | null; wasteReason?: string | null; note?: string; initial?: boolean; /** Bu satır hangi sayıma ait (gün önce). */ countDaysAgo?: number; }; export type SeedCount = { daysAgo: number; expected: number; actual: number }; /** * Haftalık simülasyon: tüketim, periyodik sayım (farkı nadiren), eşikte alım ve fire. Stok asla * eksiye düşmez. */ export function simulate(p: SeedProduct, today: string): { rows: Row[]; counts: SeedCount[] } { const rows: Row[] = []; const counts: SeedCount[] = []; const plan = COUNT_PLANS[p.category.split(".")[0]!]; let stock = p.target; let purchases = 0; rows.push({ kind: "count_adjust", daysAgo: START_DAYS_AGO, quantity: stock, initial: true }); for (let d = START_DAYS_AGO - 7; d >= 0; d -= 7) { // 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 (plan?.on(d)) { const expected = stock; const nth = counts.length + 1; if (p.countDrift && nth % p.countDrift.everyCounts === 0 && stock > 0) { const drift = Math.min(stock, Math.round((p.target * p.countDrift.pct) / 100)); rows.push({ kind: "count_adjust", daysAgo: d, quantity: -drift, note: "Sayım", countDaysAgo: d, }); stock -= drift; } counts.push({ daysAgo: d, expected, actual: stock }); } 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, counts }; } 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")!; // Periyodik sayım sıklığı (kullanıcı ayarladıysa dokunulmaz). for (const [key, plan] of Object.entries(COUNT_PLANS)) await StockCategoryModel.updateOne( { householdId, defaultKey: key, countEveryDays: null }, { $set: { countEveryDays: plan.everyDays } }, ); 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 })) { // Faz 13 öncesi seed'lenmiş veritabanında hacimli işaretini tamamla. if (p.bulky) await ProductTypeModel.updateOne( { householdId, seedRef: p.ref, bulky: { $exists: false } }, { $set: { bulky: true } }, ); 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, bulky: p.bulky ?? false, seedRef: p.ref, createdBy: actor, updatedBy: actor, }); const base = Date.now() - START_DAYS_AGO * 86_400_000; const { rows, counts } = simulate(p, today); // Sayım oturumu kategori başına ortak: ürün satırı oturuma eklenir. const root = p.category.split(".")[0]!; const countIds = new Map(); for (const c of counts) { const session = await StockCountModel.findOneAndUpdate( { householdId, seedRef: `${root}:${c.daysAgo}` }, { $setOnInsert: { householdId, date: addDays(today, -c.daysAgo), categoryId: catByKey.get(root) ?? null, note: "", seedRef: `${root}:${c.daysAgo}`, createdBy: actor, }, $push: { items: { productTypeId: doc._id, expected: c.expected, actual: c.actual, wasted: 0 }, }, }, { upsert: true, returnDocument: "after" }, ).lean<{ _id: Types.ObjectId }>(); countIds.set(c.daysAgo, session!._id); } // Ham ekleme: aynı tarihli hareketlerin sırası (createdAt) korunur. await StockMovementModel.collection.insertMany( rows.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: r.countDaysAgo === undefined ? null : (countIds.get(r.countDaysAgo) ?? null), deletedAt: null, seedRef: `${p.ref}:${i}`, createdBy: actor, createdAt: at, updatedAt: at, }; }), ); created++; } return created; }