// L17 Pakete — reine Stufenregeln (src/lib/plans.ts), ohne Datenbank. // Lauf: npx tsx scripts/test-pakete-rules.ts import { chatOverage, chatQuota, DEFAULT_TENANT_TIER, effectiveTier, isFeatureInTier, isModuleInTier, isQuotaBlocked, isTenantTier, LOTSE_CHAT_CHATS_PER_SEAT, LOTSE_CHAT_OVERAGE_PACK, modulesLockedByTier, monthKeyOfDay, nextMonthKey, overagePacks, previousMonthKey, PROFI_ONLY_MODULES, usageLevel, usagePercent, } from "../src/lib/plans"; import { MODULE_KEYS } from "../src/lib/modules"; import { NAV_ITEMS, visibleNavItems } from "../src/lib/nav"; let failures = 0; const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; console.log("— Stufen —"); ok(DEFAULT_TENANT_TIER === "PROFI", "Default-Stufe ist Profi"); ok(isTenantTier("BASIS") && isTenantTier("PROFI") && !isTenantTier("GOLD") && !isTenantTier(null), "isTenantTier erkennt nur BASIS/PROFI"); ok(effectiveTier("BASIS", "TRIAL") === "PROFI", "Testphase läuft immer als Profi"); ok(effectiveTier("BASIS", "FULL") === "BASIS" && effectiveTier("PROFI", "FULL") === "PROFI", "Vollversion: gespeicherte Stufe gilt"); ok(effectiveTier(undefined, "FULL") === "PROFI", "unbekannte Stufe → Default Profi"); console.log("\n— Module je Stufe —"); const profiOnly = ["billing", "emergency", "imports", "lotse"]; ok(JSON.stringify([...PROFI_ONLY_MODULES].sort()) === JSON.stringify([...profiOnly].sort()), "Nur-Profi-Module: billing, emergency, imports, lotse"); for (const key of MODULE_KEYS) { ok(isModuleInTier("PROFI", key), `Profi enthält ${key}`); ok(isModuleInTier("BASIS", key) === !profiOnly.includes(key), `Basis ${profiOnly.includes(key) ? "sperrt" : "enthält"} ${key}`); } ok(modulesLockedByTier("PROFI").length === 0 && modulesLockedByTier("BASIS").length === 4, "modulesLockedByTier"); ok(!isFeatureInTier("BASIS", "planning") && !isFeatureInTier("BASIS", "lotse_chat"), "Basis: keine Planung, kein Lotse-Chat"); ok(isFeatureInTier("PROFI", "planning") && isFeatureInTier("PROFI", "lotse_chat"), "Profi: Planung und Lotse-Chat"); console.log("\n— Kontingent —"); ok(LOTSE_CHAT_CHATS_PER_SEAT === 150 && LOTSE_CHAT_OVERAGE_PACK === 100, "150 Chats je Platz, Mehrverbrauch in Paketen à 100"); ok(chatQuota(3) === 450 && chatQuota(0) === 0 && chatQuota(-2) === 0, "Kontingent = Plätze × 150"); ok(chatOverage(212, 450) === 0 && chatOverage(460, 450) === 10, "Mehrverbrauch = über dem Kontingent"); ok(overagePacks(0) === 0 && overagePacks(1) === 1 && overagePacks(100) === 1 && overagePacks(101) === 2 && overagePacks(250) === 3, "angefangene 100er-Pakete zählen"); ok(usageLevel(212, 450) === "ok", "212/450 → ok"); ok(usageLevel(360, 450) === "warn" && usageLevel(359, 450) === "ok", "ab 80 % → warn"); ok(usageLevel(450, 450) === "exhausted" && usageLevel(451, 450) === "over", "ausgeschöpft / Mehrverbrauch"); ok(usageLevel(0, 0) === "ok" && usageLevel(1, 0) === "over", "ohne Kontingent zählt jede Nutzung als Mehrverbrauch"); ok(usagePercent(212, 450) === 47 && usagePercent(900, 450) === 100 && usagePercent(0, 0) === 0, "Balken in Prozent (gedeckelt)"); ok(!isQuotaBlocked(500, 450, false), "ohne hartes Limit wird weitergezählt"); ok(isQuotaBlocked(450, 450, true) && !isQuotaBlocked(449, 450, true), "hartes Limit sperrt ab dem Kontingent"); console.log("\n— Monate —"); ok(monthKeyOfDay("2026-09-21") === "2026-09", "Monat eines Tags"); ok(previousMonthKey("2026-01") === "2025-12" && previousMonthKey("2026-09") === "2026-08", "Vormonat inkl. Jahreswechsel"); ok(nextMonthKey("2026-12") === "2027-01" && nextMonthKey("2026-09") === "2026-10", "Folgemonat inkl. Jahreswechsel"); console.log("\n— Navigation —"); const all = NAV_ITEMS.flatMap((i) => i.permissions ?? []); const basisNav = visibleNavItems(NAV_ITEMS, { disabledModules: new Set(profiOnly), lockedFeatures: new Set(["planning", "lotse_chat"]), permissions: all }); const profiNav = visibleNavItems(NAV_ITEMS, { disabledModules: new Set(), lockedFeatures: new Set(), permissions: all }); ok(!basisNav.some((i) => i.href.startsWith("/planning")), "Basis: keine Planung in der Navigation"); ok(!basisNav.some((i) => ["/billing", "/imports", "/work-orders/emergency-review"].includes(i.href)), "Basis: keine Abrechnung/Import/Notdienst-Prüfung"); ok(basisNav.some((i) => i.href === "/work-orders") && basisNav.some((i) => i.href === "/work-orders/time-approvals") && basisNav.some((i) => i.href === "/reports"), "Basis: Aufträge, Zeitfreigabe, Berichte bleiben"); ok(profiNav.some((i) => i.href === "/planning") && profiNav.some((i) => i.href === "/billing"), "Profi: Planung und Abrechnung sichtbar"); console.log(failures === 0 ? "\nAlle Prüfungen grün." : `\n${failures} FEHLER.`); process.exit(failures === 0 ? 0 : 1);