Phase 10: Mandantenfähigkeit (#15)

This commit was merged in pull request #15.
This commit is contained in:
2026-09-05 13:47:46 +00:00
parent 2b53556175
commit 91742a0dd7
98 changed files with 3810 additions and 201 deletions
+245 -3
View File
@@ -57,6 +57,7 @@ import {
LebensdauerBezug,
LebensdauerKategorie,
MangelPrioritaet,
MitgliedschaftArt,
MangelStatus,
PrismaClient,
PruefErgebnis,
@@ -198,7 +199,13 @@ async function rollenAnlegen(organisationId: string, katalog: Map<string, string
const rollenKarte = new Map<string, string>();
for (const schluessel of ROLLEN) {
// AUFSICHT ist keine vergebbare Rolle und steht deshalb auch nicht im
// Prisma-Enum. Sie ergibt sich seit Phase 10 daraus, dass ein Konto einer
// Wehr als Kreisaufsicht zugeordnet ist — sie hier anzulegen hiesse, dass
// jemand sie innerhalb einer Wehr vergeben könnte.
const vergebbar = ROLLEN.filter((schluessel) => schluessel !== "AUFSICHT");
for (const schluessel of vergebbar) {
const rolle = await prisma.rolle.upsert({
where: {
organisationId_schluessel: {
@@ -499,12 +506,31 @@ async function adminAnlegen(organisationId: string, rollenKarte: Map<string, str
nachname: "Administrator",
email,
dienstgrad: Dienstgrad.KEIN,
passwortHash: await bcrypt.hash(passwort, RUNDEN),
passwortGeaendertAm: new Date(),
abteilung: "Verwaltung",
},
});
// Das Konto seit Phase 10 getrennt von der Akte. Die E-Mail ist global
// eindeutig — wird derselbe Seed für eine zweite Wehr gefahren, entsteht
// deshalb kein zweites Konto, sondern eine zweite Mitgliedschaft. Genau so
// sieht Doppelmitgliedschaft in echt aus.
const konto = await prisma.benutzer.upsert({
where: { email },
update: {},
create: {
email,
passwortHash: await bcrypt.hash(passwort, RUNDEN),
passwortGeaendertAm: new Date(),
istVerwalter: true,
},
});
await prisma.mitgliedschaft.upsert({
where: { benutzerId_organisationId: { benutzerId: konto.id, organisationId } },
update: {},
create: { organisationId, benutzerId: konto.id, personId: admin.id },
});
const rolleId = rollenKarte.get(RolleSchluessel.SUPERADMIN);
if (rolleId) {
await prisma.personRolle.upsert({
@@ -3897,6 +3923,219 @@ async function kassendatenAnlegen(organisationId: string): Promise<void> {
}
// -----------------------------------------------------------------------------
// Phase 10 — Mandantenfähigkeit
//
// Eine zweite, kleine Wehr und ein Konto, das beide sieht.
//
// Ohne die zweite Wehr lässt sich die ganze Phase nicht ausprobieren: der
// Wehrwähler erscheint erst, wenn es etwas zu wählen gibt, und eine
// Mandantentrennung, die nie zwei Mandanten hatte, ist keine geprüfte.
//
// Die Geschichte ist die übliche in Bayern: der Kommandant von Musterhausen
// ist zugleich Kreisbrandinspektor und betreut die Nachbarwehr. In seiner
// eigenen Wehr führt er alles, in der betreuten sieht er zu — mehr nicht.
//
// Nachbarshausen bleibt bewusst dünn: drei Leute, ein Fahrzeug. Es soll
// zeigen, dass die Trennung hält, und nicht die Beispieldaten verdoppeln.
// -----------------------------------------------------------------------------
/**
* Die Nachbarwehr leitet sich aus der Wehr ab, die gerade angelegt wird.
*
* Fest verdrahtet wäre sie das, was Phase 8 gerade beseitigt hat: der
* Testlauf und die Beispielwehr griffen auf denselben Datensatz zu. So bekommt
* jeder Mandant sein eigenes Paar — und die Trennung gilt auch für das, womit
* sie geprüft wird.
*/
const NACHBAR_SLUG = `${ORGANISATION_SLUG}-nachbar`;
const NACHBAR_NAME = `${ORGANISATION_NAME} — Nachbarwehr`;
const NACHBAR_KURZNAME = `${ORGANISATION_KURZNAME} Nachbar`;
const KBI_EMAIL = process.env.SEED_KBI_EMAIL ?? `kbi@${ORGANISATION_SLUG}.de`;
async function kreisebeneAnlegen(
heimatOrganisationId: string,
katalog: Map<string, string>,
): Promise<void> {
/**
* Zwei Teile, zwei Prüfungen — und das ist wichtiger, als es aussieht.
*
* Ein gemeinsamer Ausstieg am Anfang hätte heißen: steht die Nachbarwehr,
* geschieht gar nichts mehr. Genau das ist beim Zurücksetzen der Testwehr
* passiert — die Nachbarwehr blieb stehen, die Mitgliedschaft in der neuen
* Testwehr wurde nie wieder angelegt, und der Wehrwähler zeigte nur noch
* die betreute Wehr. Gefunden hat das ein E2E-Test.
*/
const nachbar =
(await prisma.organisation.findUnique({ where: { slug: NACHBAR_SLUG } })) ??
(await nachbarwehrAnlegen(katalog));
// --- Das Konto, das beide Wehren sieht ------------------------------------
await kreiskontoAnlegen(heimatOrganisationId, nachbar.id);
}
/** Die Nachbarwehr: dünn besetzt, ein Fahrzeug. Sie soll zeigen, dass die
* Trennung hält, und nicht die Beispieldaten verdoppeln. */
async function nachbarwehrAnlegen(katalog: Map<string, string>) {
const nachbar = await prisma.organisation.create({
data: {
slug: NACHBAR_SLUG,
name: NACHBAR_NAME,
kurzname: NACHBAR_KURZNAME,
strasse: "Dorfstraße 3",
plz: "91316",
ort: "Nachbarshausen",
landkreis: "Musterkreis",
funkPraefix: "Florian Nachbarshausen",
},
});
const nachbarRollen = await rollenAnlegen(nachbar.id, katalog);
const besetzung: Array<{
vorname: string;
nachname: string;
nummer: string;
dienstgrad: Dienstgrad;
rolle: RolleSchluessel;
}> = [
{
vorname: "Georg",
nachname: "Sailer",
nummer: "0001",
dienstgrad: Dienstgrad.BRANDMEISTER,
rolle: RolleSchluessel.KOMMANDANT,
},
{
vorname: "Marianne",
nachname: "Kraus",
nummer: "0002",
dienstgrad: Dienstgrad.LOESCHMEISTER,
rolle: RolleSchluessel.GERAETEWART,
},
{
vorname: "Tobias",
nachname: "Ostermeier",
nummer: "0003",
dienstgrad: Dienstgrad.FEUERWEHRMANN,
rolle: RolleSchluessel.MITGLIED,
},
];
for (const eintrag of besetzung) {
const person = await prisma.person.create({
data: {
organisationId: nachbar.id,
vorname: eintrag.vorname,
nachname: eintrag.nachname,
mitgliedsnummer: eintrag.nummer,
dienstgrad: eintrag.dienstgrad,
abteilung: "Aktive",
eintrittsdatum: datum(2012, 4, 1),
},
});
const rolleId = nachbarRollen.get(eintrag.rolle);
if (rolleId) {
await prisma.personRolle.create({
data: { organisationId: nachbar.id, personId: person.id, rolleId },
});
}
}
const standort = await prisma.standort.create({
data: {
organisationId: nachbar.id,
name: "Gerätehaus Nachbarshausen",
art: StandortArt.GERAETEHAUS,
strasse: "Dorfstraße 3",
plz: "91316",
ort: "Nachbarshausen",
},
});
await prisma.fahrzeug.create({
data: {
organisationId: nachbar.id,
funkrufname: "Florian Nachbarshausen 42/1",
bezeichnung: "TSF-W",
art: FahrzeugArt.LOESCHFAHRZEUG,
kennzeichen: "MK-FW 421",
baujahr: 2009,
standortId: standort.id,
},
});
log(`Nachbarwehr angelegt: ${besetzung.length} Personen, 1 Fahrzeug.`);
return nachbar;
}
/** Das Konto der Kreisebene: Mitglied in der Heimatwehr, Aufsicht nebenan. */
async function kreiskontoAnlegen(heimatOrganisationId: string, nachbarId: string) {
const kommandant = await prisma.person.findFirst({
where: {
organisationId: heimatOrganisationId,
rollen: { some: { rolle: { schluessel: RolleSchluessel.KOMMANDANT } } },
},
orderBy: { mitgliedsnummer: "asc" },
});
if (!kommandant) {
log("Kein Kommandant gefunden — die Kreisebene wird übersprungen.");
return;
}
const email = KBI_EMAIL;
const passwort = process.env.SEED_KBI_PASSWORT ?? "Feuerwehr2026";
const konto = await prisma.benutzer.upsert({
where: { email },
update: {},
create: {
email,
passwortHash: await bcrypt.hash(passwort, RUNDEN),
passwortGeaendertAm: new Date(),
},
});
// In der eigenen Wehr: Mitglied mit allem, was seine Rollen hergeben.
await prisma.mitgliedschaft.upsert({
where: {
benutzerId_organisationId: {
benutzerId: konto.id,
organisationId: heimatOrganisationId,
},
},
update: {},
create: {
organisationId: heimatOrganisationId,
benutzerId: konto.id,
personId: kommandant.id,
art: MitgliedschaftArt.MITGLIED,
},
});
// In der Nachbarwehr: Aufsicht. Ohne Akte — er gehört ihr nicht an und
// steht deshalb auch in keiner ihrer Mitgliederlisten.
await prisma.mitgliedschaft.upsert({
where: {
benutzerId_organisationId: { benutzerId: konto.id, organisationId: nachbarId },
},
update: {},
create: {
organisationId: nachbarId,
benutzerId: konto.id,
art: MitgliedschaftArt.AUFSICHT,
},
});
log(
`Kreisebene: ${email} — Mitglied in ${ORGANISATION_KURZNAME}, ` +
`Aufsicht in ${NACHBAR_KURZNAME}.`,
);
}
// -----------------------------------------------------------------------------
// Ablauf
// -----------------------------------------------------------------------------
@@ -3946,6 +4185,9 @@ async function main(): Promise<void> {
// Phase 9 — Kassenverwaltung
await kassendatenAnlegen(organisation.id);
// Phase 10 — Mandantenfähigkeit: eine zweite Wehr und die Kreisebene
await kreisebeneAnlegen(organisation.id, katalog);
log("Fertig.");
log("");
log(`Wehr: ${ORGANISATION_NAME} (${ORGANISATION_SLUG})`);