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>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user