L15 Testphase & Onboarding: Selbstanmeldung mit Double-Opt-in, Plattform-Wizard, Nur-Lesen-Sperre, Export, Lebenszyklus-Job
- Datenmodell: Testphasen-Lebenszyklus am Mandanten (plan, trialEndsAt, readOnlySince, deletionDueAt, Versandmarker), TrialSignup (Plattform, Hashes statt Klartext), TenantExport (RLS), Onboarding-Status - /testen: 5-Schritte-Wizard (Betrieb, Admin-Konto, Enddatum, Einrichtung, Zusammenfassung), Bestätigung per POST, direkte Anmeldung über login-ticket; Rate-Limit je IP/E-Mail, Honeypot, Enumeration-Schutz, Slug-Kollisionen - Plattform: Wizard „Testmandant anlegen“ mit Einladung, Badges/Filter, Enddatum ändern, umwandeln, beenden, Löschung vormerken/abbrechen (Bestätigung + Audit) - Schreibsperre nach Ablauf zentral in moduleGuard und requireApiContext (non-GET über withApi), Upload-Routen, Einstellungen/Nutzerverwaltung, Worker-Jobs; Banner Backoffice + mobil - Datenexport (ZIP mit CSV/JSON + Dateien) als Worker-Job, auch im Nur-Lesen-Zustand - Täglicher Job trial-lifecycle: Erinnerungen 7/3/1, Ablauf, Löschhinweis, Löschung über das Offboarding - Erste-Schritte-Checkliste im Dashboard, Mail-Vorlagen de/en, Tests + Smoke, Betriebsdoku Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// Lane L15 „Testphase & Onboarding" — Lebenszyklus-Job und Plattform-Aktionen:
|
||||
// Erinnerungen 7/3/1 Tage genau einmal (auch nach verpasstem Lauf), Ablaufmail, Löschhinweis,
|
||||
// Löschung nur fällig + TRIAL (über das bestehende Offboarding inkl. Speicher), umgewandelte,
|
||||
// verlängerte oder abgebrochene Mandanten bleiben erhalten, Mandant B unberührt;
|
||||
// Plattform-Rechte (Mandanten-Admin/Read-only-Admin können die Testphase nicht ändern),
|
||||
// Plattform-Wizard mit Einladung, Mail-Vorlagen, Worker-Registrierung.
|
||||
//
|
||||
// Lauf: npx tsx scripts/test-testphase-lifecycle.ts (lokale Postgres-DB + Garage aus .env)
|
||||
|
||||
import "dotenv/config";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { prisma, dbForTenant } from "../src/server/db";
|
||||
import { PROCESSORS } from "../src/server/jobs/processors";
|
||||
import { renderTemplate, TRIAL_TEMPLATE_KEYS, type TemplateVars } from "../src/server/mail/templates";
|
||||
import { addDaysToKey, todayKey, trialEndInstant } from "../src/lib/trial/dates";
|
||||
import {
|
||||
cancelTrialDeletion,
|
||||
changeTrialEndDate,
|
||||
convertTenantToFull,
|
||||
createPlatformTrialTenant,
|
||||
endTrialNow,
|
||||
scheduleTrialDeletion,
|
||||
} from "../src/server/services/trial/admin";
|
||||
import { deleteTrialTenant, runTrialLifecycle } from "../src/server/services/trial/lifecycle";
|
||||
import { getTrialState } from "../src/server/services/trial/state";
|
||||
import { storage } from "../src/server/storage/adapter";
|
||||
import { readStoredBytes } from "../src/server/services/documents/read";
|
||||
import { captureMail, cleanupL15, ctxFor, DOMAIN, expectCode, failures, ok, platformAdmin, trialTenant } from "./lib/testphase-fixture";
|
||||
|
||||
const HOUR = 3600_000;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
async function main() {
|
||||
await cleanupL15();
|
||||
const full = await platformAdmin("full");
|
||||
const readonly = await platformAdmin("readonly");
|
||||
const actor = { platformAdminId: full.id };
|
||||
const today = todayKey();
|
||||
|
||||
console.log("\n— Erinnerungen 7/3/1 genau einmal —");
|
||||
const R = await trialTenant("Lifecycle R", addDaysToKey(today, 7));
|
||||
const endR = trialEndInstant(addDaysToKey(today, 7));
|
||||
const at = (dayOffset: number) => new Date(endR.getTime() - DAY * 7 + dayOffset * DAY - 12 * HOUR); // noon-ish of "today + offset"
|
||||
const mail = captureMail();
|
||||
const run = (now: Date, ids: string[]) => runTrialLifecycle({ now, tenantIds: ids, sendMail: mail.fn });
|
||||
const count = (tpl: string, tenantId: string) => mail.sent.filter((m) => m.template === tpl && m.tenantId === tenantId).length;
|
||||
|
||||
await run(at(0), [R.tenantId]);
|
||||
ok(count("trial_reminder", R.tenantId) === 1 && (mail.sent[0].vars as { daysLeft: number }).daysLeft === 7, "7 Tage vorher: Erinnerung an den Admin");
|
||||
ok(mail.sent[0].to === `admin-lifecycle-r${DOMAIN}`, "Empfänger = aktiver Mandanten-Admin");
|
||||
await run(at(0), [R.tenantId]);
|
||||
await run(at(1), [R.tenantId]);
|
||||
ok(count("trial_reminder", R.tenantId) === 1, "zweiter Lauf / Folgetag: keine weitere Erinnerung");
|
||||
await run(at(4), [R.tenantId]);
|
||||
ok(count("trial_reminder", R.tenantId) === 2, "3 Tage vorher: zweite Erinnerung");
|
||||
await run(at(4), [R.tenantId]);
|
||||
await run(at(6), [R.tenantId]);
|
||||
await run(at(6), [R.tenantId]);
|
||||
ok(count("trial_reminder", R.tenantId) === 3, "1 Tag vorher: dritte Erinnerung, jeweils genau einmal");
|
||||
const markers = await prisma.tenant.findUniqueOrThrow({ where: { id: R.tenantId } });
|
||||
ok(!!markers.trialReminder7At && !!markers.trialReminder3At && !!markers.trialReminder1At, "Versandmarker gesetzt");
|
||||
|
||||
const R2 = await trialTenant("Lifecycle R2", addDaysToKey(today, 2));
|
||||
await run(new Date(), [R2.tenantId]);
|
||||
const r2 = await prisma.tenant.findUniqueOrThrow({ where: { id: R2.tenantId } });
|
||||
ok(count("trial_reminder", R2.tenantId) === 1 && !!r2.trialReminder7At && !!r2.trialReminder3At && !r2.trialReminder1At, "verpasster Lauf: nur die nächstliegende Erinnerung, ältere als erledigt markiert");
|
||||
|
||||
console.log("\n— Ablauf, Löschhinweis, Löschung —");
|
||||
const endExpired = new Date(endR.getTime() + HOUR);
|
||||
await run(endExpired, [R.tenantId]);
|
||||
await run(endExpired, [R.tenantId]);
|
||||
ok(count("trial_expired", R.tenantId) === 1, "Ablaufmail am Endtag genau einmal");
|
||||
const expiredRow = await prisma.tenant.findUniqueOrThrow({ where: { id: R.tenantId } });
|
||||
ok(expiredRow.readOnlySince?.getTime() === endR.getTime(), "readOnlySince = Ende der Testphase");
|
||||
const expiredMail = mail.sent.find((m) => m.template === "trial_expired" && m.tenantId === R.tenantId)!;
|
||||
ok(!!(expiredMail.vars as { deletionDate?: string }).deletionDate && (expiredMail.vars as { exportUrl: string }).exportUrl.endsWith("/settings/export"), "Ablaufmail nennt Löschdatum und Export");
|
||||
const due = expiredRow.deletionDueAt!;
|
||||
ok(due.getTime() === endR.getTime() + 30 * DAY, "Löschung 30 Tage nach Ende fällig");
|
||||
await run(new Date(due.getTime() - 8 * DAY), [R.tenantId]);
|
||||
ok(count("trial_deletion_notice", R.tenantId) === 0, "8 Tage vor Löschung noch kein Hinweis");
|
||||
await run(new Date(due.getTime() - 6 * DAY), [R.tenantId]);
|
||||
await run(new Date(due.getTime() - 5 * DAY), [R.tenantId]);
|
||||
ok(count("trial_deletion_notice", R.tenantId) === 1, "Löschhinweis 7 Tage vorher genau einmal");
|
||||
|
||||
// tenant data + stored object that must disappear
|
||||
const dbR = dbForTenant(R.tenantId);
|
||||
const customerR = await dbR.customer.create({ data: { tenantId: R.tenantId, companyName: "ZZ Löschkunde", city: "Bremen" } });
|
||||
const stored = await storage.put({ tenantId: R.tenantId, filename: "zz-loeschen.txt", contentType: "text/plain", bytes: Buffer.from("zz") });
|
||||
const identityR = await prisma.user.findFirstOrThrow({ where: { tenantId: R.tenantId }, select: { identityId: true } });
|
||||
|
||||
// untouched neighbours: FULL tenant B (via provisioning then converted), not due trial B2
|
||||
const B = await trialTenant("Lifecycle B", addDaysToKey(today, 3));
|
||||
await convertTenantToFull(actor, B.tenantId);
|
||||
const customerB = await dbForTenant(B.tenantId).customer.create({ data: { tenantId: B.tenantId, companyName: "ZZ Kunde B", city: "Kiel" } });
|
||||
const B2 = await trialTenant("Lifecycle B2", addDaysToKey(today, 20));
|
||||
|
||||
ok(!(await deleteTrialTenant(R.tenantId, { now: new Date(due.getTime() - HOUR) })).deleted, "vor Fälligkeit: keine Löschung");
|
||||
const summary = await run(new Date(due.getTime() + HOUR), [R.tenantId, B.tenantId, B2.tenantId]);
|
||||
ok(summary.deleted.length === 1 && summary.deleted[0] === R.tenantId, "fälliger Testmandant gelöscht, nur dieser");
|
||||
const gone = await prisma.tenant.findUniqueOrThrow({ where: { id: R.tenantId } });
|
||||
ok(gone.status === "ARCHIVED" && !!gone.trialDeletedAt && gone.deletionDueAt === null, "Mandant archiviert, Löschung protokolliert am Mandanten");
|
||||
ok((await prisma.customer.count({ where: { tenantId: R.tenantId } })) === 0 && (await prisma.user.count({ where: { tenantId: R.tenantId } })) === 0 && (await prisma.role.count({ where: { tenantId: R.tenantId } })) === 0, "alle Mandanten-Tabellen geleert (Topologie)");
|
||||
ok((await prisma.identity.findUnique({ where: { id: identityR.identityId } })) === null, "Identity ohne weitere Mitgliedschaft gelöscht");
|
||||
ok((await readStoredBytes(stored.storageKey)) === null, "Speicherobjekte unter <tenantId>/ entfernt");
|
||||
ok((await prisma.deletionCertificate.count({ where: { tenantId: R.tenantId, scope: "tenant" } })) === 1, "Löschnachweis erstellt");
|
||||
ok((await prisma.auditLog.count({ where: { scope: "platform", entity: "trial_tenant", entityId: R.tenantId, action: "delete" } })) === 1, "Plattform-Audit der Löschung");
|
||||
ok((await prisma.customer.findUnique({ where: { id: customerR.id } })) === null, "Kunde des gelöschten Mandanten entfernt");
|
||||
ok((await dbForTenant(B.tenantId).customer.count({ where: { id: customerB.id } })) === 1 && (await prisma.tenant.findUniqueOrThrow({ where: { id: B.tenantId } })).status === "ACTIVE", "Mandant B (Vollversion) unberührt");
|
||||
ok((await prisma.tenant.findUniqueOrThrow({ where: { id: B2.tenantId } })).trialDeletedAt === null, "nicht fälliger Testmandant unberührt");
|
||||
await run(new Date(due.getTime() + 2 * HOUR), [R.tenantId]);
|
||||
ok((await prisma.auditLog.count({ where: { scope: "platform", entity: "trial_tenant", entityId: R.tenantId } })) === 1, "gelöschter Mandant wird nicht erneut verarbeitet");
|
||||
|
||||
console.log("\n— Umwandeln, Verlängern, Abbrechen verhindern die Löschung —");
|
||||
const X = await trialTenant("Lifecycle X", addDaysToKey(today, 1));
|
||||
await endTrialNow(actor, X.tenantId);
|
||||
const xDue = (await prisma.tenant.findUniqueOrThrow({ where: { id: X.tenantId } })).deletionDueAt!;
|
||||
await convertTenantToFull(actor, X.tenantId);
|
||||
const xRow = await prisma.tenant.findUniqueOrThrow({ where: { id: X.tenantId } });
|
||||
ok(xRow.plan === "FULL" && !!xRow.convertedAt && xRow.deletionDueAt === null && !(await getTrialState(X.tenantId)).readOnly, "Umwandeln: Vollversion, schreibbar, keine Löschung vorgemerkt");
|
||||
await run(new Date(xDue.getTime() + DAY), [X.tenantId]);
|
||||
ok(!(await deleteTrialTenant(X.tenantId, { now: new Date(xDue.getTime() + DAY) })).deleted && (await prisma.tenant.findUniqueOrThrow({ where: { id: X.tenantId } })).status === "ACTIVE", "umgewandelter Mandant wird nie gelöscht (Doppelprüfung)");
|
||||
|
||||
const Y = await trialTenant("Lifecycle Y", addDaysToKey(today, 1));
|
||||
await endTrialNow(actor, Y.tenantId);
|
||||
const yOldDue = (await prisma.tenant.findUniqueOrThrow({ where: { id: Y.tenantId } })).deletionDueAt!;
|
||||
await changeTrialEndDate(actor, Y.tenantId, addDaysToKey(today, 60));
|
||||
const yRow = await prisma.tenant.findUniqueOrThrow({ where: { id: Y.tenantId } });
|
||||
ok(yRow.trialEndsAt!.getTime() === trialEndInstant(addDaysToKey(today, 60)).getTime() && yRow.deletionDueAt!.getTime() > yOldDue.getTime() && yRow.readOnlySince === null && yRow.trialExpiredNoticeAt === null, "Verlängern: neues Ende, Löschtermin verschoben, Marker zurückgesetzt");
|
||||
await run(new Date(yOldDue.getTime() + DAY), [Y.tenantId]);
|
||||
ok((await prisma.tenant.findUniqueOrThrow({ where: { id: Y.tenantId } })).trialDeletedAt === null, "verlängerter Mandant nach altem Löschtermin nicht gelöscht");
|
||||
|
||||
const Z = await trialTenant("Lifecycle Z", addDaysToKey(today, 1));
|
||||
await endTrialNow(actor, Z.tenantId);
|
||||
const zDue = (await prisma.tenant.findUniqueOrThrow({ where: { id: Z.tenantId } })).deletionDueAt!;
|
||||
await cancelTrialDeletion(actor, Z.tenantId);
|
||||
await run(new Date(zDue.getTime() + 10 * DAY), [Z.tenantId]);
|
||||
const zRow = await prisma.tenant.findUniqueOrThrow({ where: { id: Z.tenantId } });
|
||||
ok(zRow.trialDeletedAt === null && (await getTrialState(Z.tenantId, new Date(zDue.getTime() + 10 * DAY))).readOnly, "Löschung abgebrochen: bleibt erhalten, weiter nur lesbar");
|
||||
await scheduleTrialDeletion(actor, Z.tenantId);
|
||||
const zScheduled = (await prisma.tenant.findUniqueOrThrow({ where: { id: Z.tenantId } })).deletionDueAt!;
|
||||
ok(zScheduled.getTime() >= Date.now() + 7 * DAY - HOUR, "Löschung vormerken: frühestens in 7 Tagen (Hinweis möglich)");
|
||||
await changeTrialEndDate(actor, Z.tenantId, addDaysToKey(today, 3));
|
||||
ok((await prisma.tenant.findUniqueOrThrow({ where: { id: Z.tenantId } })).deletionDueAt!.getTime() === trialEndInstant(addDaysToKey(today, 3)).getTime() + 30 * DAY, "vorgemerkte Löschung folgt dem neuen Enddatum");
|
||||
const auditZ = await prisma.auditLog.findMany({ where: { tenantId: Z.tenantId, scope: "platform", entity: { startsWith: "trial_" } }, orderBy: { createdAt: "asc" } });
|
||||
ok(auditZ.map((a) => a.entity).join(",") === "trial_ended,trial_deletion_cancelled,trial_deletion_scheduled,trial_end_date" && auditZ.every((a) => a.actorId === full.id && a.before && a.after), "Plattform-Audit je Aktion mit before/after");
|
||||
|
||||
console.log("\n— Plattform-Rechte —");
|
||||
const P = await trialTenant("Lifecycle P", addDaysToKey(today, 5));
|
||||
const tenantAdminActor = { platformAdminId: P.adminUserId };
|
||||
const endBefore = (await prisma.tenant.findUniqueOrThrow({ where: { id: P.tenantId } })).trialEndsAt!.getTime();
|
||||
await expectCode(() => changeTrialEndDate(tenantAdminActor, P.tenantId, addDaysToKey(today, 30)), "forbidden", "Mandanten-Admin kann die Testphase nicht verlängern");
|
||||
await expectCode(() => convertTenantToFull(tenantAdminActor, P.tenantId), "forbidden", "Mandanten-Admin kann nicht umwandeln");
|
||||
await expectCode(() => cancelTrialDeletion(tenantAdminActor, P.tenantId), "forbidden", "Mandanten-Admin kann die Löschung nicht abbrechen");
|
||||
await expectCode(() => changeTrialEndDate({ platformAdminId: readonly.id }, P.tenantId, addDaysToKey(today, 30)), "forbidden", "Read-only-Plattform-Admin kann nichts ändern");
|
||||
await expectCode(() => createPlatformTrialTenant(tenantAdminActor, { companyName: "ZZ L15 Hack", adminName: "Hack", adminEmail: `hack${DOMAIN}`, endDate: addDaysToKey(today, 5), sampleData: false }), "forbidden", "Mandanten-Admin kann keinen Testmandanten anlegen");
|
||||
await expectCode(() => changeTrialEndDate(actor, P.tenantId, addDaysToKey(today, -1)), "invalid", "Enddatum in der Vergangenheit → invalid");
|
||||
await expectCode(() => changeTrialEndDate(actor, P.tenantId, addDaysToKey(today, 400)), "invalid", "Enddatum > 1 Jahr → invalid");
|
||||
await expectCode(() => changeTrialEndDate(actor, X.tenantId, addDaysToKey(today, 10)), "invalid", "Vollversion: keine Testphasen-Aktion");
|
||||
ok((await prisma.tenant.findUniqueOrThrow({ where: { id: P.tenantId } })).trialEndsAt!.getTime() === endBefore, "Testphase von P unverändert");
|
||||
const { trialLifecycleAction } = await import("../src/server/actions/trial-platform");
|
||||
const fd = new FormData();
|
||||
fd.set("confirm", "on");
|
||||
fd.set("endDate", addDaysToKey(today, 40));
|
||||
await expectCode(() => trialLifecycleAction(P.tenantId, "extend", { status: "idle" }, fd), "*", "Action ohne Plattform-Session wird abgewiesen");
|
||||
ok((await prisma.tenant.findUniqueOrThrow({ where: { id: P.tenantId } })).trialEndsAt!.getTime() === endBefore, "… und ändert nichts");
|
||||
const platformSrc = readFileSync("src/server/actions/trial-platform.ts", "utf8");
|
||||
const exportsP = [...platformSrc.matchAll(/export async function (\w+)/g)].map((m) => m[1]);
|
||||
ok(exportsP.length === 2 && exportsP.every((n) => new RegExp(`export async function ${n}[\\s\\S]*?await requirePlatformFullAdmin\\(\\)`).test(platformSrc)), "jede Plattform-Action verlangt einen Plattform-Voll-Admin");
|
||||
const tenantSrc = readFileSync("src/server/actions/trial-tenant.ts", "utf8");
|
||||
ok(!/services\/trial\/admin/.test(tenantSrc) && !/trialEndsAt|deletionDueAt|convertedAt/.test(tenantSrc), "Mandanten-Actions enthalten keine Testphasen-Änderung");
|
||||
|
||||
console.log("\n— Plattform-Wizard mit Einladung —");
|
||||
const invites: { to: string; tenantId: string }[] = [];
|
||||
const created = await createPlatformTrialTenant(actor, { companyName: "ZZ L15 Wizard GmbH", sector: "Elektro", adminName: "Wanda Wizard", adminEmail: `wanda${DOMAIN}`, endDate: addDaysToKey(today, 45), sampleData: true }, { invite: async (i) => void invites.push({ to: i.to, tenantId: i.tenantId }) });
|
||||
const w = await prisma.tenant.findUniqueOrThrow({ where: { id: created.tenantId } });
|
||||
ok(w.plan === "TRIAL" && w.trialSource === "platform" && w.trialEndsAt!.getTime() === trialEndInstant(addDaysToKey(today, 45)).getTime(), "Wizard: Testmandant mit frei gesetztem Enddatum (> TRIAL_MAX_DAYS)");
|
||||
ok(created.invited && invites.length === 1 && invites[0].to === `wanda${DOMAIN}` && invites[0].tenantId === w.id, "Einladung über den bestehenden Einladungsweg");
|
||||
const wId = await prisma.identity.findUniqueOrThrow({ where: { email: `wanda${DOMAIN}` } });
|
||||
ok(wId.mustChangePassword, "neue Identity: Passwort wird über die Einladung gesetzt");
|
||||
ok((await prisma.authToken.count({ where: { principalId: wId.id, type: "invitation", usedAt: null } })) === 1, "Einladungs-Token ausgestellt (Hash)");
|
||||
ok((await dbForTenant(w.id).workOrder.count()) === 4, "Wizard mit Beispieldaten");
|
||||
const again = await createPlatformTrialTenant(actor, { companyName: "ZZ L15 Wizard Zwei", adminName: "Wanda Wizard", adminEmail: `wanda${DOMAIN}`, endDate: addDaysToKey(today, 10), sampleData: false }, { invite: async (i) => void invites.push({ to: i.to, tenantId: i.tenantId }) });
|
||||
ok(!again.invited && invites.length === 1 && again.identityId === wId.id, "bestehende Person: nur verknüpft, keine neue Einladung");
|
||||
ok((await prisma.auditLog.count({ where: { tenantId: w.id, entity: "trial", actorId: full.id } })) === 1, "Audit mit handelndem Plattform-Admin");
|
||||
|
||||
console.log("\n— Vorlagen und Worker —");
|
||||
const sample: { [K in (typeof TRIAL_TEMPLATE_KEYS)[number]]: TemplateVars[K] } = {
|
||||
trial_confirm: { name: "Paula", companyName: "Muster", trialEnd: "30.09.2026", actionUrl: "https://x.example/testen/bestaetigen?token=abc", expires: "16.09.2026, 10:00" },
|
||||
trial_existing_account: { name: "Paula", loginUrl: "https://x.example/login", resetUrl: "https://x.example/forgot-password" },
|
||||
trial_reminder: { name: "Paula", tenantName: "Muster", daysLeft: 3, endDate: "30.09.2026", actionUrl: "https://x.example/dashboard", contact: "vertrieb@craftvia.example" },
|
||||
trial_expired: { name: "Paula", tenantName: "Muster", endDate: "30.09.2026", deletionDate: "30.10.2026", exportUrl: "https://x.example/settings/export" },
|
||||
trial_deletion_notice: { name: "Paula", tenantName: "Muster", deletionDate: "30.10.2026", exportUrl: "https://x.example/settings/export" },
|
||||
};
|
||||
for (const key of TRIAL_TEMPLATE_KEYS) {
|
||||
for (const locale of ["de", "en"] as const) {
|
||||
const r = renderTemplate(key, locale, sample[key] as never);
|
||||
ok(r.subject.length > 5 && !/undefined|\{/.test(r.subject + r.text) && r.html.includes("<"), `Vorlage ${key} (${locale}) rendert`);
|
||||
}
|
||||
}
|
||||
ok(renderTemplate("trial_reminder", "de", { ...sample.trial_reminder, daysLeft: 1 }).subject.includes("morgen") && renderTemplate("trial_reminder", "de", { ...sample.trial_reminder, daysLeft: 0 }).subject.includes("heute"), "Erinnerung: „morgen“/„heute“");
|
||||
ok(typeof PROCESSORS["trial-lifecycle"] === "function" && typeof PROCESSORS["tenant-export"] === "function", "Processor trial-lifecycle und tenant-export registriert");
|
||||
ok(readFileSync("src/server/jobs/queues.ts", "utf8").includes('"trial-lifecycle-daily"'), "täglicher Job-Scheduler angelegt");
|
||||
const ctxP = ctxFor(P.tenantId, P.adminUserId, "tenant-admin");
|
||||
ok((await ctxP.db.tenantSettings.findFirst())?.orgName === "ZZ L15 Lifecycle P", "Mandanten-Einstellungen provisioniert");
|
||||
|
||||
await cleanupL15();
|
||||
await prisma.$disconnect();
|
||||
console.log(failures ? `\n${failures} Prüfung(en) fehlgeschlagen` : "\nAlle Prüfungen bestanden");
|
||||
process.exit(failures ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(async (err) => {
|
||||
console.error(err);
|
||||
await cleanupL15().catch(() => undefined);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user