feat: create and invite educators in a single form

This commit is contained in:
t.indorf
2026-10-09 21:59:04 +02:00
parent 979f795341
commit 4702bf5abb
8 changed files with 785 additions and 177 deletions
+269
View File
@@ -709,3 +709,272 @@ test("Entzogene Rollen, fehlender Consent und gesperrte Accounts können keine P
});
await assert.rejects(f.run(f.staff, "time", time()), /keinen Zugriff/);
});
function onboarding(email = `${randomUUID()}@staff-test.invalid`) {
return {
firstName: " Neue ",
lastName: " Fachkraft ",
active: true,
email,
employmentStart: "2024-08-01",
trackingStart: "2026-10-09",
};
}
test("Gemeinsame Anlage speichert Fachkraft, Profil, Account und wiederholbaren Mailauftrag", async () => {
const f = await fixture();
const input = onboarding();
const result = await executeStaffCommand(f.admin, {
command: "educatorCreate",
data: input,
});
const educator = await prisma.educator.findUniqueOrThrow({
where: { id: result.educatorId },
include: { staffProfile: { include: { user: true } } },
});
assert.equal(educator.firstName, "Neue");
assert.equal(educator.kitaId, f.kita.id);
assert.equal(educator.staffProfile?.user?.email, input.email);
assert.equal(educator.staffProfile?.user?.emailVerifiedAt, null);
assert.equal(educator.staffProfile?.user?.role, "ERZIEHER");
assert.equal(
educator.staffProfile?.employmentStart.toISOString().slice(0, 10),
"2024-08-01",
);
assert.equal(
educator.staffProfile?.trackingStart.toISOString().slice(0, 10),
"2026-10-09",
);
assert.equal(educator.staffProfile?.openingMinutes, 0);
assert.ok(result.invite);
assert.ok(
await prisma.verificationToken.findUnique({
where: { token: result.invite.token },
}),
);
assert.equal(
(
await deliverStaffEmails(result.emailIds!, async () => ({
success: false,
error: "Testausfall",
}))
).failed,
1,
);
assert.ok(await prisma.educator.findUnique({ where: { id: educator.id } }));
const retry = await executeStaffCommand(f.admin, {
command: "emailRetry",
data: {},
});
assert.deepEqual(retry.emailIds, result.emailIds);
assert.equal(
(
await deliverStaffEmails(retry.emailIds!, async () => ({
success: true,
id: "mock",
}))
).sent,
1,
);
});
test("Pflichtfelder und Erfassungsdatum verhindern unvollständige oder falsche Anlagen", async () => {
const f = await fixture();
const before = await prisma.educator.count({ where: { kitaId: f.kita.id } });
for (const input of [
onboarding(""),
{ ...onboarding(), firstName: " " },
{ ...onboarding(), trackingStart: "2024-07-31" },
{ ...onboarding(), employmentStart: "2026-02-30" },
]) {
await assert.rejects(
executeStaffCommand(f.admin, { command: "educatorCreate", data: input }),
);
}
assert.equal(
await prisma.educator.count({ where: { kitaId: f.kita.id } }),
before,
);
});
test("Doppelte oder fremde Accounts rollen die gesamte Anlage zurück", async () => {
const f = await fixture(),
foreign = await fixture();
const parent = await prisma.user.findUniqueOrThrow({
where: { id: f.parent.id },
});
const before = await prisma.educator.count({ where: { kitaId: f.kita.id } });
for (const email of [
f.staffUser.email,
foreign.staffUser.email,
parent.email,
]) {
await assert.rejects(
executeStaffCommand(f.admin, {
command: "educatorCreate",
data: onboarding(email),
}),
/bereits/,
);
}
assert.equal(
await prisma.educator.count({ where: { kitaId: f.kita.id } }),
before,
);
});
test("Freier aktivierter Fachkraft-Account derselben Kita wird verknüpft", async () => {
const f = await fixture();
const user = await prisma.user.update({
where: { id: f.other.id },
data: { emailVerifiedAt: new Date() },
});
const result = await executeStaffCommand(f.admin, {
command: "educatorCreate",
data: onboarding(user.email.toUpperCase()),
});
const profile = await prisma.staffProfile.findUniqueOrThrow({
where: { educatorId: result.educatorId },
});
assert.equal(profile.userId, user.id);
assert.equal(result.invite, undefined);
assert.equal(result.emailIds, undefined);
});
test("Bestehende Fachkraft ohne Profil erhält Zugang im Bearbeitungsablauf", async () => {
const f = await fixture();
const educator = await prisma.educator.create({
data: { firstName: "Alt", lastName: "Ohne Profil", kitaId: f.kita.id },
});
const input = onboarding();
await assert.rejects(
executeStaffCommand(f.admin, {
command: "educatorUpdate",
data: { ...input, educatorId: educator.id, email: f.staffUser.email },
}),
/bereits/,
);
assert.equal(
(await prisma.educator.findUniqueOrThrow({ where: { id: educator.id } }))
.firstName,
"Alt",
);
const result = await executeStaffCommand(f.admin, {
command: "educatorUpdate",
data: { ...input, educatorId: educator.id },
});
assert.equal(result.educatorId, educator.id);
assert.ok(result.invite);
assert.equal(
await prisma.staffProfile.count({ where: { educatorId: educator.id } }),
1,
);
});
test("Bearbeiten schützt verknüpfte Identität, Zeiten und Konten", async () => {
const f = await fixture();
await assert.rejects(
executeStaffCommand(f.admin, {
command: "educatorUpdate",
data: {
...onboarding(),
educatorId: f.educator.id,
},
}),
/nicht geändert/,
);
await executeStaffCommand(f.admin, {
command: "educatorUpdate",
data: {
...onboarding(f.staffUser.email),
firstName: "Korrigiert",
educatorId: f.educator.id,
employmentStart: "2025-01-01",
trackingStart: "2025-01-01",
},
});
const profile = await getStaffProfile(f.admin, f.profile.id);
assert.equal(profile?.openingMinutes, 600);
assert.equal(
profile?.employmentStart.toISOString().slice(0, 10),
"2026-01-01",
);
assert.equal(profile?.workPatterns.length, 1);
assert.equal(profile?.leaveAccounts.length, 1);
assert.equal(profile?.user?.firstName, "Korrigiert");
});
test("Einladung aus Teamliste erneuert nur eigene ausstehende Aktivierung", async () => {
const f = await fixture(),
foreign = await fixture();
const result = await executeStaffCommand(f.admin, {
command: "educatorCreate",
data: onboarding(),
});
for (const actor of [f.staff, f.parent, foreign.admin]) {
await assert.rejects(
executeStaffCommand(actor, {
command: "educatorInvite",
data: { educatorId: result.educatorId },
}),
);
}
const renewed = await executeStaffCommand(f.admin, {
command: "educatorInvite",
data: { educatorId: result.educatorId },
});
assert.ok(renewed.invite);
assert.notEqual(renewed.invite.token, result.invite?.token);
assert.equal(
await prisma.verificationToken.count({
where: { token: result.invite?.token },
}),
0,
);
const profile = await prisma.staffProfile.findUniqueOrThrow({
where: { educatorId: result.educatorId },
});
await prisma.user.update({
where: { id: profile.userId! },
data: { emailVerifiedAt: new Date() },
});
await assert.rejects(
executeStaffCommand(f.admin, {
command: "educatorInvite",
data: { educatorId: result.educatorId },
}),
/bereits einen Account/,
);
});
test("Nur aktuelle Kita-Admins dürfen Fachkräfte anlegen und bearbeiten", async () => {
const f = await fixture();
const before = await prisma.educator.count({ where: { kitaId: f.kita.id } });
for (const actor of [
f.staff,
f.parent,
{ ...f.staff, role: UserRole.ADMIN },
]) {
await assert.rejects(
executeStaffCommand(actor, {
command: "educatorCreate",
data: onboarding(),
}),
);
}
const foreign = await fixture();
await assert.rejects(
executeStaffCommand(foreign.admin, {
command: "educatorUpdate",
data: {
...onboarding(f.staffUser.email),
educatorId: f.educator.id,
},
}),
/nicht gefunden/,
);
assert.equal(
await prisma.educator.count({ where: { kitaId: f.kita.id } }),
before,
);
});