import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { AuditLogModel } from "@/models/audit-log"; import { ProductModel } from "@/models/product"; import { ProductTypeModel } from "@/models/product-type"; import { ShoppingListModel } from "@/models/shopping-list"; import { shoppingItemFormSchema, shoppingItemUpdateSchema } from "@/shared/schemas/shopping"; import { setupSeededDb, teardownDb } from "@/test/seeded-db"; import { addShoppingItem, approveSuggestions, createShoppingList, deleteShoppingList, dismissSuggestion, getShoppingList, listShoppingLists, moveShoppingItem, removeShoppingItem, setShoppingItemChecked, updateShoppingItem, } from "./service"; let ids: Awaited>; let cakir = ""; let ayse = ""; let zeynep = ""; let fatma = ""; let weekly = ""; let monthEnd = ""; const typeId = (seedRef: string) => ProductTypeModel.findOne({ seedRef }) .lean() .then((x) => String(x!._id)); const productId = (seedRef: string) => ProductModel.findOne({ seedRef }) .lean() .then((x) => String(x!._id)); const item = (input: Record) => shoppingItemFormSchema.parse(input); beforeAll(async () => { ids = await setupSeededDb(); cakir = ids.household("Çakır Ailesi"); ayse = ids.user("ayse"); zeynep = ids.user("zeynep"); fatma = ids.user("fatma"); weekly = String((await ShoppingListModel.findOne({ seedRef: "haftalik" }).lean())!._id); monthEnd = String((await ShoppingListModel.findOne({ seedRef: "ay-sonu" }).lean())!._id); }); afterAll(teardownDb); describe("liste okuma ve akıllı öneri", () => { it("aktif listeler, bekleyen öneri sayısı; başka hane göremez", async () => { const data = (await listShoppingLists(ayse, cakir))!; expect(data.active.map((l) => l.name).sort()).toEqual(["Ay sonu toptan", "Haftalık market"]); expect(data.suggestions).toBeGreaterThanOrEqual(3); expect(data.canWrite).toBe(true); expect(await listShoppingLists(fatma, cakir)).toBeNull(); expect(await getShoppingList(fatma, cakir, weekly)).toBeNull(); }); it("kalem görünümleri, eşik önerileri ve plan", async () => { const d = (await getShoppingList(ayse, cakir, weekly))!; expect(d.items).toHaveLength(9); const milk = d.items.find((i) => i.label === "Süt")!; expect(milk).toMatchObject({ kind: "type", unit: "lt", quantityLabel: "4 lt" }); expect(milk.stock).not.toBeNull(); const yumos = d.items.find((i) => i.kind === "product")!; expect(yumos.quantityLabel).toBe("1 paket"); const candle = d.items.find((i) => i.kind === "text")!; expect(candle).toMatchObject({ label: "Doğum günü mumu", note: "Rakam mum: 7" }); // Eşiğin altındaki mercimek listede yok: öneri olarak gelir. expect(d.suggestions.map((s) => s.name)).toContain("Kırmızı Mercimek"); expect(d.suggestions.every((s) => s.suggestedQuantity >= 0)).toBe(true); // Plan: domatesin fiyatı yok; serbest metin plana katılmaz; diğerleri karşılanır. const tomato = d.items.find((i) => i.label === "Domates")!; expect(d.plan.unpriced).toEqual([tomato.id]); const rec = d.plan.recommended!; expect(rec.missing).toEqual([]); const planned = rec.stores.flatMap((g) => g.lines.map((l) => l.itemId)); expect(planned).toHaveLength(7); expect(planned).not.toContain(candle.id); for (const g of rec.stores) { expect(d.plan.stores[g.storeId]).toBeDefined(); for (const l of g.lines) expect(d.plan.products[l.productId]).toBeDefined(); } // Belirli marka kalemi yalnızca o ürünle planlanır. const yumosLine = rec.stores.flatMap((g) => g.lines).find((l) => l.itemId === yumos.id)!; expect(yumosLine.productId).toBe(yumos.productId); expect(d.plan.single).not.toBeNull(); expect(d.plan.thresholdMinor).toBe(10_000); // Ayşe'nin yemek kartı bakiyesi plana girer (seed: Multinet). expect(d.plan.mealCardBalanceMinor).toBeGreaterThan(0); expect(d.options.types.length).toBeGreaterThan(10); expect(d.otherLists.map((o) => o.name)).toEqual(["Ay sonu toptan"]); }); }); describe("kalem işlemleri", () => { it("ekle (çift kayıt reddi, izleyici yazamaz), düzenle, işaretle, taşı, çıkar", async () => { const milk = await typeId("sut"); expect(shoppingItemFormSchema.safeParse({ kind: "type", productTypeId: "" }).success).toBe( false, ); expect(shoppingItemFormSchema.safeParse({ kind: "text", text: "" }).success).toBe(false); expect( (await addShoppingItem(ayse, cakir, weekly, item({ kind: "type", productTypeId: milk }))).ok, ).toBe(false); const text = item({ kind: "text", text: "Pil", quantity: "4", unit: "adet" }); expect((await addShoppingItem(zeynep, cakir, weekly, text)).ok).toBe(false); const added = await addShoppingItem(ayse, cakir, weekly, text); expect(added.ok).toBe(true); const id = added.ok ? added.data.id : ""; const brand = await addShoppingItem( ayse, cakir, weekly, item({ kind: "product", productId: await productId("ariel-5kg"), quantity: "2" }), ); expect(brand.ok).toBe(true); expect( ( await updateShoppingItem( ayse, cakir, weekly, id, shoppingItemUpdateSchema.parse({ quantity: "6", text: "AA pil", unit: "adet" }), ) ).ok, ).toBe(true); let d = (await getShoppingList(ayse, cakir, weekly))!; expect(d.items.find((i) => i.id === id)).toMatchObject({ label: "AA pil", quantityLabel: "6 adet", }); const ariel = d.items.find((i) => i.label.startsWith("Ariel"))!; expect(ariel.quantityLabel).toBe("2 paket"); expect((await setShoppingItemChecked(ayse, cakir, weekly, id, true)).ok).toBe(true); d = (await getShoppingList(ayse, cakir, weekly))!; // Sepete alınanlar sona. expect(d.items.at(-1)).toMatchObject({ id, checked: true }); expect(d.list.checkedCount).toBe(1); expect((await moveShoppingItem(ayse, cakir, weekly, id, weekly)).ok).toBe(false); expect((await moveShoppingItem(ayse, cakir, weekly, id, monthEnd)).ok).toBe(true); const target = (await getShoppingList(ayse, cakir, monthEnd))!; expect(target.items.find((i) => i.id === id)).toMatchObject({ checked: false }); expect((await removeShoppingItem(ayse, cakir, monthEnd, id)).ok).toBe(true); expect((await removeShoppingItem(ayse, cakir, monthEnd, id)).ok).toBe(false); expect((await removeShoppingItem(ayse, cakir, weekly, ariel.id)).ok).toBe(true); }); }); describe("eşik önerileri", () => { it("onayla: önerilen miktarla eklenir, öneriden düşer; tekrar onay reddedilir", async () => { const lentil = await typeId("kirmizi-mercimek"); const r = await approveSuggestions(ayse, cakir, weekly, [lentil]); expect(r).toMatchObject({ ok: true, data: { added: 1 } }); const d = (await getShoppingList(ayse, cakir, weekly))!; const row = d.items.find((i) => i.productTypeId === lentil)!; expect(row.source).toBe("suggestion"); expect(row.quantity).toBeGreaterThan(0); expect(d.suggestions.some((s) => s.productTypeId === lentil)).toBe(false); expect((await approveSuggestions(ayse, cakir, weekly, [lentil])).ok).toBe(false); // Başka aktif listede de öneri olarak görünmez. const other = (await getShoppingList(ayse, cakir, monthEnd))!; expect(other.suggestions.some((s) => s.productTypeId === lentil)).toBe(false); await removeShoppingItem(ayse, cakir, weekly, row.id); }); it("reddet: stok yenilenene kadar gizli; izleyici reddedemez", async () => { const tablet = await typeId("tablet-deterjan"); expect((await dismissSuggestion(zeynep, cakir, tablet)).ok).toBe(false); expect((await dismissSuggestion(ayse, cakir, tablet)).ok).toBe(true); const d = (await getShoppingList(ayse, cakir, weekly))!; expect(d.suggestions.some((s) => s.productTypeId === tablet)).toBe(false); await ProductTypeModel.updateOne( { seedRef: "tablet-deterjan" }, { $set: { suggestionDismissedAt: null } }, ); }); }); describe("liste yönetimi", () => { it("oluştur ve sil (soft delete + denetim)", async () => { const r = await createShoppingList(ayse, cakir, { name: "Deneme", note: "" }); expect(r.ok).toBe(true); const id = r.ok ? r.data.id : ""; expect((await deleteShoppingList(zeynep, cakir, id)).ok).toBe(false); expect((await deleteShoppingList(ayse, cakir, id)).ok).toBe(true); expect(await getShoppingList(ayse, cakir, id)).toBeNull(); expect( await AuditLogModel.exists({ action: "shoppingList.delete", targetId: id }), ).toBeTruthy(); }); });