Eine echte Proxmox-Rechnung zeigt je Subscription-Key eine Zeile mit Listenpreis und eine mit "Client Discount". Unser Modell hing den Rabatt an die Rechnung - bei einer Sammelrechnung ueber zwei Hosts liesse er sich damit keinem Host zuordnen, und die Marge je Host waere falsch. Schlimmer: Er wurde nirgends abgezogen. Jetzt haengt der Rabatt an der Position, der Rechnungsbetrag ist die Summe nach Abzug, und Marge wie Summen rechnen mit dem tatsaechlich gezahlten Betrag. Die Migration verteilt vorhandene Rabatte anteilig auf die Positionen und rechnet die Summen neu - nichts geht verloren. Dazu ein eigener Steuersatz fuer den Einkauf mit Vorgabe 0: Proxmox rechnet gegenueber Unternehmen mit USt-IdNr. ueber Reverse Charge ab. Neue zwoelfte Warnregel aus Florians Ablauf: Der Kunde hat bezahlt, die Proxmox-Rechnung ist noch offen - der Moment, an dem man selbst zahlt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { alertsAbgleichen, warnregelnSicherstellen } from "@/server/alerts/abgleich";
|
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
|
|
const JETZT = new Date("2026-09-17T12:00:00Z");
|
|
|
|
async function hostMitAblauf(tage: number) {
|
|
return testPrisma.host.create({
|
|
data: {
|
|
agentId: "agent-1",
|
|
hostname: "pve-01",
|
|
produkt: "PVE",
|
|
subStatus: "ACTIVE",
|
|
subKey: "pve1c-aaaa",
|
|
subNextduedate: new Date(JETZT.getTime() + tage * 86_400_000),
|
|
letzterReportAm: JETZT,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Abgleich der Warnungen", () => {
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
await warnregelnSicherstellen();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testPrisma.$disconnect();
|
|
});
|
|
|
|
it("legt die Standard-Einstellungen für alle Regeln an", async () => {
|
|
const einstellungen = await testPrisma.warnregelEinstellung.findMany();
|
|
expect(einstellungen).toHaveLength(12);
|
|
const ablauf = einstellungen.find((e) => e.regel === "subscription-laeuft-ab");
|
|
expect(ablauf?.schwellwerte).toEqual({ tage: [30, 14, 7] });
|
|
});
|
|
|
|
it("öffnet eine Warnung und meldet sie nicht doppelt", async () => {
|
|
await hostMitAblauf(5);
|
|
|
|
const erster = await alertsAbgleichen(JETZT);
|
|
expect(erster.geoeffnet.map((a) => a.regel)).toContain("subscription-laeuft-ab");
|
|
|
|
const zweiter = await alertsAbgleichen(JETZT);
|
|
expect(zweiter.geoeffnet).toHaveLength(0);
|
|
|
|
const offene = await testPrisma.alert.findMany({ where: { erledigtAm: null } });
|
|
expect(offene.filter((a) => a.regel === "subscription-laeuft-ab")).toHaveLength(1);
|
|
});
|
|
|
|
it("schließt eine Warnung, sobald die Ursache behoben ist", async () => {
|
|
const host = await hostMitAblauf(5);
|
|
await alertsAbgleichen(JETZT);
|
|
|
|
// Verlängert: neues Ablaufdatum weit in der Zukunft.
|
|
await testPrisma.host.update({
|
|
where: { id: host.id },
|
|
data: { subNextduedate: new Date(JETZT.getTime() + 400 * 86_400_000) },
|
|
});
|
|
|
|
const ergebnis = await alertsAbgleichen(JETZT);
|
|
expect(ergebnis.erledigt).toContain(`subscription-laeuft-ab:${host.id}:7`);
|
|
|
|
const alert = await testPrisma.alert.findFirstOrThrow({
|
|
where: { fingerprint: `subscription-laeuft-ab:${host.id}:7` },
|
|
});
|
|
expect(alert.erledigtAm).not.toBeNull();
|
|
});
|
|
|
|
it("öffnet einen erledigten Fall erneut, wenn er wieder auftritt", async () => {
|
|
const host = await hostMitAblauf(5);
|
|
await alertsAbgleichen(JETZT);
|
|
await testPrisma.host.update({
|
|
where: { id: host.id },
|
|
data: { subNextduedate: new Date(JETZT.getTime() + 400 * 86_400_000) },
|
|
});
|
|
await alertsAbgleichen(JETZT);
|
|
|
|
await testPrisma.host.update({
|
|
where: { id: host.id },
|
|
data: { subNextduedate: new Date(JETZT.getTime() + 5 * 86_400_000) },
|
|
});
|
|
const ergebnis = await alertsAbgleichen(JETZT);
|
|
|
|
expect(ergebnis.geoeffnet.map((a) => a.fingerprint)).toContain(`subscription-laeuft-ab:${host.id}:7`);
|
|
const alerts = await testPrisma.alert.findMany({
|
|
where: { fingerprint: `subscription-laeuft-ab:${host.id}:7` },
|
|
});
|
|
// Derselbe Eintrag wird wiederverwendet, es entsteht kein zweiter.
|
|
expect(alerts).toHaveLength(1);
|
|
expect(alerts[0].erledigtAm).toBeNull();
|
|
});
|
|
|
|
it("führt die Beschreibung nach, wenn sich der Fall ändert", async () => {
|
|
const host = await hostMitAblauf(5);
|
|
await alertsAbgleichen(JETZT);
|
|
|
|
const spaeter = new Date(JETZT.getTime() + 2 * 86_400_000);
|
|
await alertsAbgleichen(spaeter);
|
|
|
|
const alert = await testPrisma.alert.findFirstOrThrow({
|
|
where: { fingerprint: `subscription-laeuft-ab:${host.id}:7` },
|
|
});
|
|
expect(alert.titel).toBe("Subscription läuft in 3 Tagen ab");
|
|
});
|
|
|
|
it("steigt von der 30- auf die 7-Tage-Stufe", async () => {
|
|
const host = await hostMitAblauf(25);
|
|
const erster = await alertsAbgleichen(JETZT);
|
|
expect(erster.geoeffnet.some((a) => a.fingerprint.endsWith(":30"))).toBe(true);
|
|
|
|
const spaeter = new Date(JETZT.getTime() + 20 * 86_400_000);
|
|
const zweiter = await alertsAbgleichen(spaeter);
|
|
|
|
expect(zweiter.geoeffnet.some((a) => a.fingerprint.endsWith(":7"))).toBe(true);
|
|
expect(zweiter.erledigt).toContain(`subscription-laeuft-ab:${host.id}:30`);
|
|
});
|
|
|
|
it("beachtet abgeschaltete Regeln", async () => {
|
|
await testPrisma.host.create({
|
|
data: { agentId: "agent-2", hostname: "pve-ohne-kunde", produkt: "PVE", letzterReportAm: JETZT },
|
|
});
|
|
|
|
await testPrisma.warnregelEinstellung.update({
|
|
where: { regel: "host-ohne-kunde" },
|
|
data: { aktiv: false },
|
|
});
|
|
|
|
const ergebnis = await alertsAbgleichen(JETZT);
|
|
expect(ergebnis.geoeffnet.some((a) => a.regel === "host-ohne-kunde")).toBe(false);
|
|
});
|
|
});
|