Files
ProxLedger/tests/laufzeit-bearbeiten.test.ts
T
florian.wildeandClaude Opus 5 83320f0443 feat(laufzeiten): anlegen, bearbeiten und zusammenfuehren (#043)
Laufzeiten entstanden bisher nur als Vorschlag aus dem Agenten-Report;
bestaetigen oder verwerfen war alles. Anlegen von Hand gab es als Aktion ohne
Oberflaeche, Aendern und Zusammenfuehren gar nicht - obwohl der Doppelfall
zwangslaeufig auftritt, sobald das gemeldete Ablaufdatum von der Bestellung
abweicht.

Jetzt: Formular zum Anlegen auf der Rechnungsseite (Key vorgeschlagen, Ende
aus dem Beginn vorbelegt), Bearbeiten von Key, Zeitraum und Notiz im
Laufzeit-Detail, und Zusammenfuehren zweier Laufzeiten desselben Hosts. Die
aufgeloeste gibt ihre Rechnungen ab, sofern das Ziel dort nichts hat; sonst
bricht der Vorgang mit Erklaerung ab - zwei Einkaufsrechnungen auf einem
Lizenzjahr waeren nicht mehr zuzuordnen.

Dazu die Kundenansicht: Laufzeit und Host verlinkt, Marge und Rabatt je Zeile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 20:57:07 +02:00

129 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/** Laufzeiten nachträglich ändern und zusammenführen (#043). */
vi.mock("@/server/auth/sitzungspflicht", () => ({
sitzungErforderlich: async () => ({ benutzerId: "test", rolle: "ADMIN", kundeId: null }),
sichtbarerKunde: async () => null,
darfEinkaufSehen: async () => true,
angemeldet: async () => null,
}));
const { laufzeitBearbeiten, laufzeitenZusammenfuehren, RechnungsFehler } = await import(
"@/server/services/rechnungen"
);
async function host(name = "hv-pve-01") {
return testPrisma.host.create({ data: { agentId: `a-${name}`, hostname: name, produkt: "PVE" } });
}
async function laufzeit(hostId: string, start: string, key = "pve2s-aaaa") {
return testPrisma.laufzeit.create({
data: {
hostId,
key,
start: new Date(start),
ende: new Date(new Date(start).setUTCFullYear(new Date(start).getUTCFullYear() + 1)),
status: "VORSCHLAG",
},
});
}
async function mitEinkauf(laufzeitId: string, nummer: string) {
const rechnung = await testPrisma.proxmoxRechnung.create({
data: { nummer, datum: new Date("2026-09-17"), nettoBetrag: 990, ustSatz: 0 },
});
const position = await testPrisma.proxmoxRechnungPosition.create({
data: { rechnungId: rechnung.id, bezeichnung: "Position", nettoBetrag: 1100, rabatt: 110 },
});
await testPrisma.laufzeit.update({
where: { id: laufzeitId },
data: { proxmoxPosition: { connect: { id: position.id } } },
});
}
describe.runIf(datenbankVerfuegbar)("Laufzeit bearbeiten", () => {
beforeEach(async () => {
await datenbankLeeren();
});
afterAll(async () => {
await testPrisma.$disconnect();
});
it("ändert Key und Zeitraum und bestätigt dabei den Vorschlag", async () => {
const h = await host();
const l = await laufzeit(h.id, "2026-10-17");
await laufzeitBearbeiten(l.id, {
key: "pve2s-neu",
start: new Date("2026-10-18"),
ende: new Date("2027-10-17"),
notizen: "Datum laut Bestellung",
});
const danach = await testPrisma.laufzeit.findUniqueOrThrow({ where: { id: l.id } });
expect(danach.key).toBe("pve2s-neu");
expect(danach.start.toISOString().slice(0, 10)).toBe("2026-10-18");
expect(danach.status).toBe("BESTAETIGT");
});
it("weist ein Ende vor dem Beginn ab", async () => {
const h = await host();
const l = await laufzeit(h.id, "2026-10-17");
await expect(
laufzeitBearbeiten(l.id, { key: "pve2s-aaaa", start: new Date("2026-10-17"), ende: new Date("2026-10-01") }),
).rejects.toBeInstanceOf(RechnungsFehler);
});
it("weist ein Startdatum ab, das am selben Host schon belegt ist", async () => {
const h = await host();
const ersteLaufzeit = await laufzeit(h.id, "2026-10-17");
await laufzeit(h.id, "2025-10-17");
await expect(
laufzeitBearbeiten(ersteLaufzeit.id, {
key: "pve2s-aaaa",
start: new Date("2025-10-17"),
ende: new Date("2026-10-16"),
}),
).rejects.toBeInstanceOf(RechnungsFehler);
});
it("führt zwei Laufzeiten zusammen und nimmt die Rechnung mit", async () => {
const h = await host();
const ziel = await laufzeit(h.id, "2026-10-17");
const quelle = await laufzeit(h.id, "2026-10-18");
await mitEinkauf(quelle.id, "291017");
await laufzeitenZusammenfuehren(ziel.id, quelle.id);
const danach = await testPrisma.laufzeit.findUniqueOrThrow({ where: { id: ziel.id } });
expect(danach.proxmoxPositionId).not.toBeNull();
expect(danach.status).toBe("BESTAETIGT");
expect(await testPrisma.laufzeit.count()).toBe(1);
// Die Rechnung selbst bleibt erhalten – nur die doppelte Laufzeit verschwindet.
expect(await testPrisma.proxmoxRechnungPosition.count()).toBe(1);
});
it("verweigert das Zusammenführen, wenn beide eine Einkaufsrechnung haben", async () => {
const h = await host();
const ziel = await laufzeit(h.id, "2026-10-17");
const quelle = await laufzeit(h.id, "2026-10-18");
await mitEinkauf(ziel.id, "291017");
await mitEinkauf(quelle.id, "291018");
await expect(laufzeitenZusammenfuehren(ziel.id, quelle.id)).rejects.toBeInstanceOf(RechnungsFehler);
expect(await testPrisma.laufzeit.count()).toBe(2);
});
it("führt nichts über Hostgrenzen hinweg zusammen", async () => {
const [a, b] = await Promise.all([host("hv-pve-01"), host("hv-pve-02")]);
const ziel = await laufzeit(a.id, "2026-10-17");
const quelle = await laufzeit(b.id, "2026-10-17");
await expect(laufzeitenZusammenfuehren(ziel.id, quelle.id)).rejects.toBeInstanceOf(RechnungsFehler);
});
});