import { beforeEach, describe, expect, it, vi } from "vitest"; import { getAdminActiveUserKpiCounts, getAdminTenantKpiCounts } from "@hivegrid/db/kpi.server"; import { StatChange } from "~/application/dtos/stats/StatChange"; import { getDbClient } from "@hivegrid/db/client.server"; import { listAuditEvents } from "@hivegrid/db/audit-events-query.server"; import { getAdminDashboardOverview, getAdminDashboardStats } from "./adminDashboardService"; import { getMrr } from "./.server/subscriptionService"; import { adminGetAllTenantsIdsAndNames } from "../db/tenants.db.server"; import { getRlsExecutionContext, runWithRlsContext } from "@hivegrid/db/rls-context.server"; import { getPlatformOutboxCounts } from "@hivegrid/db/admin-dashboard.server"; vi.mock("@hivegrid/db/kpi.server", () => ({ getAdminActiveUserKpiCounts: vi.fn(), getAdminTenantKpiCounts: vi.fn(), })); vi.mock("@hivegrid/db/client.server", () => ({ getDbClient: vi.fn(), })); vi.mock("@hivegrid/db/audit-events-query.server", () => ({ listAuditEvents: vi.fn(), })); vi.mock("@hivegrid/db/admin-dashboard.server", () => ({ getPlatformOutboxCounts: vi.fn(), })); vi.mock("./.server/subscriptionService", () => ({ getMrr: vi.fn(), })); vi.mock("../db/tenants.db.server", () => ({ adminGetAllTenantsIdsAndNames: vi.fn(), })); const adminTenants = ["tenant_1", "tenant_2", "tenant_3", "tenant_4", "tenant_5"].map((id) => ({ id, name: id, slug: id, })); const translations: Record = { "admin.kpi.accounts.name": "Translated accounts", "admin.kpi.accounts.hint": "Translated total accounts", "admin.kpi.mrr.name": "Translated MRR", "admin.kpi.mrr.hint": "Translated monthly revenue", "admin.kpi.mrr.previousStat": "{{0}} translated accounts", "admin.kpi.activeUsers.name": "Translated MAU", "admin.kpi.activeUsers.hint": "Translated monthly active users", }; function t(key: string) { return translations[key] ?? key; } describe("getAdminDashboardStats", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(adminGetAllTenantsIdsAndNames).mockResolvedValue(adminTenants); vi.mocked(getMrr).mockResolvedValue({ total: 1234.5, count: 7 }); }); it("builds translated KPI stats from package counts and forwards the selected period", async () => { const gte = new Date("2026-01-01T00:00:00.000Z"); vi.mocked(getAdminTenantKpiCounts).mockResolvedValue({ added: 3, total: 10 }); vi.mocked(getAdminActiveUserKpiCounts).mockResolvedValue({ added: 2, total: 8 }); const stats = await getAdminDashboardStats({ t, gte }); expect(getAdminTenantKpiCounts).toHaveBeenCalledTimes(1); expect(getAdminTenantKpiCounts).toHaveBeenCalledWith({ gte }); expect(getAdminActiveUserKpiCounts).toHaveBeenCalledTimes(1); expect(getAdminActiveUserKpiCounts).toHaveBeenCalledWith({ gte }); expect(getMrr).toHaveBeenCalledTimes(1); expect(getMrr).toHaveBeenCalledWith( "eur", adminTenants.map((tenant) => tenant.id) ); expect(stats).toEqual([ { name: "Translated accounts", hint: "Translated total accounts", stat: "10", previousStat: "7", change: "+3", changeType: StatChange.Increase, path: "/accounts", }, { name: "Translated MRR", hint: "Translated monthly revenue", stat: "€1,234.50", previousStat: "7 translated accounts", }, { name: "Translated MAU", hint: "Translated monthly active users", stat: "8", previousStat: "6", change: "+2", changeType: StatChange.Increase, }, ]); }); it("keeps zero additions visible with equal change type and missing translations as key text", async () => { vi.mocked(getAdminTenantKpiCounts).mockResolvedValue({ added: 0, total: 5 }); vi.mocked(getAdminActiveUserKpiCounts).mockResolvedValue({ added: 0, total: 0 }); vi.mocked(getMrr).mockResolvedValue({ total: 0, count: 0 }); const stats = await getAdminDashboardStats({ t: (key) => key }); expect(getAdminTenantKpiCounts).toHaveBeenCalledWith({ gte: undefined }); expect(getAdminActiveUserKpiCounts).toHaveBeenCalledWith({ gte: undefined }); expect(stats[0]).toMatchObject({ name: "admin.kpi.accounts.name", hint: "admin.kpi.accounts.hint", stat: "5", previousStat: "5", change: "+0", changeType: StatChange.Equal, path: "/accounts", }); expect(stats[1]).toMatchObject({ name: "admin.kpi.mrr.name", hint: "admin.kpi.mrr.hint", stat: "€0.00", previousStat: "admin.kpi.mrr.previousStat", }); expect(stats[2]).toMatchObject({ name: "admin.kpi.activeUsers.name", hint: "admin.kpi.activeUsers.hint", stat: "0", previousStat: "0", change: "+0", changeType: StatChange.Increase, }); }); }); describe("getAdminDashboardOverview", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(adminGetAllTenantsIdsAndNames).mockResolvedValue(adminTenants); vi.stubEnv("BULLMQ_REDIS_URL", "redis://localhost:6379/0"); vi.stubEnv("MEILI_URL", "http://localhost:7700"); vi.stubEnv("MEILI_API_KEY", "devkey"); vi.stubEnv("LAGO_API_URL", "http://lago.local"); vi.stubEnv("LAGO_API_KEY", "lago-key"); vi.stubEnv("MOLLIE_API_KEY", "mollie-key"); vi.stubEnv("MOLLIE_PROFILE_ID", "profile-id"); vi.stubEnv("SENTRY_ENVIRONMENT", "staging"); vi.stubEnv("SENTRY_RELEASE", "abc123"); vi.stubEnv("SENTRY_DSN", "https://example.invalid/1"); vi.mocked(listAuditEvents).mockResolvedValue({ rows: [ { id: "audit-1", createdAt: new Date("2026-07-01T10:00:00.000Z"), summaryText: "Tenant aktualisiert", action: "tenant.update", actorEmail: "ops@example.test", targetType: "Tenant", sourceModule: "admin", actorType: "user", outcome: "success", }, ], total: 1, take: 5, skip: 0, } as never); vi.mocked(getPlatformOutboxCounts).mockResolvedValue({ pendingOrProcessing: 5, failed: 6 }); }); it("builds billing, system, deploy, and audit overview signals", async () => { vi.mocked(getDbClient).mockReturnValue(createDashboardClient({ failedWebhookCount: 4, }) as never); const overview = await runWithRlsContext({ userId: "operator_1" }, () => getAdminDashboardOverview()); expect(overview.billing).toMatchObject({ openIssueCount: 6, pastDueCount: 2, frozenCount: 1, missingProviderCount: 3, }); expect(overview.billing.signals).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "billing-issues", value: "6", tone: "warning", href: "/system/billing" }), expect.objectContaining({ id: "billing-past-due", value: "2", tone: "danger", href: "/system/billing" }), ]) ); expect(overview.system.signals).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "redis", value: "OK", tone: "success" }), expect.objectContaining({ id: "webhooks", value: "4", tone: "warning", href: "/system/webhooks" }), expect.objectContaining({ id: "outbox", value: "5/6", tone: "danger", href: "/events" }), ]) ); expect(overview.deploy).toMatchObject({ environment: "staging", release: "abc123" }); expect(overview.recentEvents).toEqual([ expect.objectContaining({ id: "audit-1", label: "Tenant aktualisiert", detail: "ops@example.test · Tenant", timestamp: "2026-07-01T10:00:00.000Z", href: "/audit", }), ]); expect(listAuditEvents).toHaveBeenCalledWith({ take: 5, skip: 0 }); expect(getPlatformOutboxCounts).toHaveBeenCalledWith(undefined, { allowMarkedLocalDatabaseFallback: false, }); }); }); function createDashboardClient({ failedWebhookCount, }: { failedWebhookCount: number; }) { const subscriptions = new Map([ ["tenant_1", { status: "past_due", pastDueSince: null, frozenAt: null, planCode: "pro", lagoCustomerId: null, mollieCustomerId: "m_1" }], ["tenant_2", { status: "trial", pastDueSince: new Date(), frozenAt: null, planCode: null, lagoCustomerId: "l_2", mollieCustomerId: "m_2" }], ["tenant_3", { status: "frozen", pastDueSince: null, frozenAt: new Date(), planCode: "pro", lagoCustomerId: "l_3", mollieCustomerId: null }], ["tenant_4", { status: "active", pastDueSince: null, frozenAt: null, planCode: "pro", lagoCustomerId: null, mollieCustomerId: "m_4" }], ["tenant_5", { status: "trial", pastDueSince: null, frozenAt: null, planCode: null, lagoCustomerId: "l_5", mollieCustomerId: "m_5" }], ]); const tenantSubscriptionFindUnique = vi.fn(async ({ where }: { where: { tenantId: string } }) => { expect(getRlsExecutionContext()).toMatchObject({ userId: "operator_1", tenantId: where.tenantId }); return subscriptions.get(where.tenantId) ?? null; }); return { tenantSubscription: { findUnique: tenantSubscriptionFindUnique, }, eventWebhookAttempt: { count: vi.fn().mockResolvedValue(failedWebhookCount), }, }; }