feat(vorlagen): Abschnitte nur fuer bestimmte Abteilungen (#58)
This commit was merged in pull request #58.
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { LaufStatus, SchrittZustand, VersionStatus } from "@/generated/prisma/enums";
|
||||
import { LaufFehler, laufAnlegen, laufAbschliessen } from "@/server/services/laeufe";
|
||||
import { geltendeAbschnitteErmitteln, nurGeltende } from "@/lib/geltende-abschnitte";
|
||||
import { laufZumAbarbeiten } from "@/server/queries/laeufe";
|
||||
import { laufZumDrucken } from "@/server/queries/druck";
|
||||
import { schrittergebnisSetzen } from "@/server/services/schrittergebnisse";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* Abteilungsspezifische Zusatzabschnitte (#031).
|
||||
*
|
||||
* Die Auswahl wird **beim Start festgeschrieben**. Alles hier dreht sich darum:
|
||||
* dass sie stimmt, dass sie sich danach nicht mehr aendert, dass ein
|
||||
* ausgefilterter Pflichtschritt den Abschluss nicht blockiert, und dass die
|
||||
* Druckansicht genau dasselbe zeigt wie die Abarbeitung.
|
||||
*/
|
||||
|
||||
describe("Auswahlregel", () => {
|
||||
const abschnitte = [
|
||||
{ id: "a-alle", nurFuerAbteilungen: [] },
|
||||
{ id: "a-buch", nurFuerAbteilungen: ["Buchhaltung"] },
|
||||
{ id: "a-beide", nurFuerAbteilungen: ["Buchhaltung", "Vertrieb"] },
|
||||
];
|
||||
|
||||
it("nimmt uneingeschränkte Abschnitte immer", () => {
|
||||
expect(geltendeAbschnitteErmitteln(abschnitte, null)).toEqual(["a-alle"]);
|
||||
});
|
||||
|
||||
it("nimmt den Abschnitt der passenden Abteilung dazu", () => {
|
||||
expect(geltendeAbschnitteErmitteln(abschnitte, "Buchhaltung")).toEqual(["a-alle", "a-buch", "a-beide"]);
|
||||
expect(geltendeAbschnitteErmitteln(abschnitte, "Vertrieb")).toEqual(["a-alle", "a-beide"]);
|
||||
});
|
||||
|
||||
it("vergleicht ohne Rücksicht auf Groß- und Kleinschreibung und Ränder", () => {
|
||||
// Die Namen kommen aus zwei Quellen – der gepflegten Abteilung und dem
|
||||
// Freitextfeld im Editor. „buchhaltung“ ist dieselbe Abteilung.
|
||||
expect(geltendeAbschnitteErmitteln(abschnitte, " buchhaltung ")).toContain("a-buch");
|
||||
});
|
||||
|
||||
it("lässt eine unbekannte Abteilung nur die uneingeschränkten Abschnitte", () => {
|
||||
expect(geltendeAbschnitteErmitteln(abschnitte, "Einkauf")).toEqual(["a-alle"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nurGeltende", () => {
|
||||
const abschnitte = [{ id: "eins" }, { id: "zwei" }, { id: "drei" }];
|
||||
|
||||
it("gibt bei leerer Liste alles zurück – so bleiben alte Läufe unberührt", () => {
|
||||
expect(nurGeltende(abschnitte, [])).toEqual(abschnitte);
|
||||
});
|
||||
|
||||
it("filtert und behält die Reihenfolge der Version", () => {
|
||||
expect(nurGeltende(abschnitte, ["drei", "eins"])).toEqual([{ id: "eins" }, { id: "drei" }]);
|
||||
});
|
||||
});
|
||||
|
||||
async function aufbau(
|
||||
abschnitte: readonly {
|
||||
readonly titel: string;
|
||||
readonly nurFuerAbteilungen?: readonly string[];
|
||||
readonly schritte: readonly { readonly titel: string; readonly pflicht?: boolean }[];
|
||||
}[],
|
||||
) {
|
||||
const kunde = await testPrisma.kunde.create({
|
||||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||
});
|
||||
await testPrisma.einstellung.create({ data: { id: "global" } });
|
||||
const admin = await testPrisma.benutzer.create({
|
||||
data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" },
|
||||
});
|
||||
const buchhaltung = await testPrisma.abteilung.create({
|
||||
data: { kundeId: kunde.id, name: "Buchhaltung" },
|
||||
});
|
||||
const vertrieb = await testPrisma.abteilung.create({
|
||||
data: { kundeId: kunde.id, name: "Vertrieb" },
|
||||
});
|
||||
|
||||
const vorlage = await testPrisma.vorlage.create({ data: { name: "Onboarding" } });
|
||||
const version = await testPrisma.vorlagenVersion.create({
|
||||
data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF },
|
||||
});
|
||||
for (const [position, abschnitt] of abschnitte.entries()) {
|
||||
const angelegt = await testPrisma.vorlagenAbschnitt.create({
|
||||
data: {
|
||||
versionId: version.id,
|
||||
titel: abschnitt.titel,
|
||||
position,
|
||||
nurFuerAbteilungen: [...(abschnitt.nurFuerAbteilungen ?? [])],
|
||||
},
|
||||
});
|
||||
await testPrisma.vorlagenSchritt.createMany({
|
||||
data: abschnitt.schritte.map((schritt, stelle) => ({
|
||||
abschnittId: angelegt.id,
|
||||
titel: schritt.titel,
|
||||
pflicht: schritt.pflicht ?? false,
|
||||
position: stelle,
|
||||
})),
|
||||
});
|
||||
}
|
||||
await testPrisma.vorlagenVersion.update({
|
||||
where: { id: version.id },
|
||||
data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() },
|
||||
});
|
||||
|
||||
return {
|
||||
kunde,
|
||||
admin: { id: admin.id, rolle: admin.rolle },
|
||||
buchhaltung,
|
||||
vertrieb,
|
||||
versionId: version.id,
|
||||
};
|
||||
}
|
||||
|
||||
const VORLAGE = [
|
||||
{ titel: "Vorbereitung", schritte: [{ titel: "Papiere", pflicht: true }] },
|
||||
{
|
||||
titel: "Buchhaltung",
|
||||
nurFuerAbteilungen: ["Buchhaltung"],
|
||||
schritte: [{ titel: "DATEV einrichten", pflicht: true }],
|
||||
},
|
||||
{ titel: "Abschluss", schritte: [{ titel: "Übergabe", pflicht: true }] },
|
||||
] as const;
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Abteilungsabschnitte am Lauf", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
it("nimmt den Buchhaltungs-Abschnitt für einen Lauf der Buchhaltung", async () => {
|
||||
const { admin, buchhaltung, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: buchhaltung.id }, admin);
|
||||
|
||||
const daten = await laufZumAbarbeiten(lauf.id, admin);
|
||||
expect(daten?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual([
|
||||
"Vorbereitung",
|
||||
"Buchhaltung",
|
||||
"Abschluss",
|
||||
]);
|
||||
// Die Nummerierung zählt nur die geltenden Schritte.
|
||||
expect(daten?.abschnitte.flatMap((a) => a.schritte.map((s) => s.nummer))).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("lässt ihn für einen Lauf des Vertriebs weg", async () => {
|
||||
const { admin, vertrieb, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: vertrieb.id }, admin);
|
||||
|
||||
const daten = await laufZumAbarbeiten(lauf.id, admin);
|
||||
expect(daten?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual(["Vorbereitung", "Abschluss"]);
|
||||
expect(daten?.anzahlPflicht).toBe(2);
|
||||
});
|
||||
|
||||
it("gibt einem Lauf ohne Abteilung nur die uneingeschränkten Abschnitte", async () => {
|
||||
// Ein Firewall-Rollout hat keine Abteilung, und das ist kein Mangel.
|
||||
const { admin, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, admin);
|
||||
|
||||
const daten = await laufZumAbarbeiten(lauf.id, admin);
|
||||
expect(daten?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual(["Vorbereitung", "Abschluss"]);
|
||||
});
|
||||
|
||||
it("legt nur für die geltenden Schritte ein Schrittergebnis an", async () => {
|
||||
const { admin, vertrieb, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: vertrieb.id }, admin);
|
||||
|
||||
const ergebnisse = await testPrisma.schrittergebnis.findMany({
|
||||
where: { laufId: lauf.id },
|
||||
include: { vorlagenSchritt: true },
|
||||
});
|
||||
expect(ergebnisse.map((e) => e.vorlagenSchritt.titel).sort()).toEqual(["Papiere", "Übergabe"]);
|
||||
});
|
||||
|
||||
it("ändert die Abschnitte nicht, wenn später die Abteilung des Laufs wechselt", async () => {
|
||||
/*
|
||||
* Der Grund für das Festschreiben: ein archivierter Lauf muss genau die
|
||||
* Schritte zeigen, die damals abgearbeitet wurden. Ein Abteilungswechsel
|
||||
* darf keinen Schritt entfernen, an dem schon eine Notiz hängt.
|
||||
*/
|
||||
const { admin, buchhaltung, vertrieb, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: buchhaltung.id }, admin);
|
||||
expect((await laufZumAbarbeiten(lauf.id, admin))?.abschnitte).toHaveLength(3);
|
||||
|
||||
await testPrisma.lauf.update({ where: { id: lauf.id }, data: { abteilungId: vertrieb.id } });
|
||||
|
||||
const nachher = await laufZumAbarbeiten(lauf.id, admin);
|
||||
expect(nachher?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual([
|
||||
"Vorbereitung",
|
||||
"Buchhaltung",
|
||||
"Abschluss",
|
||||
]);
|
||||
});
|
||||
|
||||
it("lässt den Abschluss zu, obwohl ein ausgefilterter Pflichtschritt existiert", async () => {
|
||||
// Ohne diese Regel wäre der Abschluss unerreichbar: der Pflichtschritt der
|
||||
// Buchhaltung ließe sich in einem Vertriebs-Lauf nie abhaken.
|
||||
const { admin, vertrieb, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: vertrieb.id }, admin);
|
||||
|
||||
const ergebnisse = await testPrisma.schrittergebnis.findMany({ where: { laufId: lauf.id } });
|
||||
for (const ergebnis of ergebnisse) {
|
||||
await schrittergebnisSetzen(
|
||||
{
|
||||
laufId: lauf.id,
|
||||
vorlagenSchrittId: ergebnis.vorlagenSchrittId,
|
||||
zustand: SchrittZustand.ERLEDIGT,
|
||||
},
|
||||
admin,
|
||||
);
|
||||
}
|
||||
|
||||
const abgeschlossen = await laufAbschliessen(lauf.id, admin);
|
||||
expect(abgeschlossen.status).toBe(LaufStatus.ABGESCHLOSSEN);
|
||||
});
|
||||
|
||||
it("zeigt in der Druckansicht genau dieselben Abschnitte", async () => {
|
||||
const { admin, vertrieb, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId, abteilungId: vertrieb.id }, admin);
|
||||
|
||||
const abarbeitung = await laufZumAbarbeiten(lauf.id, admin);
|
||||
const druck = await laufZumDrucken(lauf.id, admin);
|
||||
expect(druck?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual(
|
||||
abarbeitung?.abschnitte.map((abschnitt) => abschnitt.titel),
|
||||
);
|
||||
expect(druck?.abschnitte.map((abschnitt) => abschnitt.titel)).not.toContain("Buchhaltung");
|
||||
});
|
||||
|
||||
it("weist einen Lauf ab, für den kein Abschnitt übrig bleibt", async () => {
|
||||
const { admin, versionId } = await aufbau([
|
||||
{
|
||||
titel: "Nur Buchhaltung",
|
||||
nurFuerAbteilungen: ["Buchhaltung"],
|
||||
schritte: [{ titel: "DATEV" }],
|
||||
},
|
||||
]);
|
||||
await expect(laufAnlegen({ vorlagenVersionId: versionId }, admin)).rejects.toThrow(LaufFehler);
|
||||
await expect(laufAnlegen({ vorlagenVersionId: versionId }, admin)).rejects.toThrow(/kein Abschnitt/);
|
||||
});
|
||||
|
||||
it("lässt einen Lauf ohne Einschränkungen unverändert – geltendeAbschnitte zählt alle", async () => {
|
||||
// Kein Sonderfall im Leseweg: die Liste enthält auch dann alle Abschnitte.
|
||||
const { admin, versionId } = await aufbau([
|
||||
{ titel: "Eins", schritte: [{ titel: "a" }] },
|
||||
{ titel: "Zwei", schritte: [{ titel: "b" }] },
|
||||
]);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, admin);
|
||||
const zeile = await testPrisma.lauf.findUniqueOrThrow({ where: { id: lauf.id } });
|
||||
expect(zeile.geltendeAbschnitte).toHaveLength(2);
|
||||
expect((await laufZumAbarbeiten(lauf.id, admin))?.abschnitte).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("zeigt bei einem Lauf von vor dieser Fassung alle Abschnitte", async () => {
|
||||
// `geltendeAbschnitte` leer heißt alle – so verhalten sich bestehende Läufe
|
||||
// unverändert, ohne dass eine Migration Daten nachführen müsste.
|
||||
const { admin, versionId } = await aufbau(VORLAGE);
|
||||
const lauf = await laufAnlegen({ vorlagenVersionId: versionId }, admin);
|
||||
await testPrisma.lauf.update({ where: { id: lauf.id }, data: { geltendeAbschnitte: [] } });
|
||||
|
||||
const daten = await laufZumAbarbeiten(lauf.id, admin);
|
||||
expect(daten?.abschnitte.map((abschnitt) => abschnitt.titel)).toEqual([
|
||||
"Vorbereitung",
|
||||
"Buchhaltung",
|
||||
"Abschluss",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user