feat(quick-260914-eym): Mail-Transport je Versand nach Mandant (WINDOWS #30), ldap/digest/matching ueber Systemkontext, vier Tabellen im Werkzeug, Erlaubnisliste vollstaendig

- mail: MailerModule-Fabrik und DB-Startpfad (findFirst beim Boot) ersatzlos
  entfernt; MailService baut je Versand einen nodemailer-Transport aus
  getDecryptedSmtpConfig(tenantId) des Empfaenger-Mandanten, Umgebungs-Kette
  (MAIL_* -> TESSERA_SMTP_* -> localhost:1025) nur als Rueckfall; Fehler
  weiter verschluckt (T-02-12), close() im finally; neue mail.service.spec.ts
  (4 Tests, T-GWH-03 geschlossen)
- settings: Startpfad-Methode samt vier Spec-Tests geloescht;
  auth: requestPasswordReset reicht user.tenantId durch (Spec-Zusicherung)
- ldap: getAllActiveConfigs und Nachverschluesselung lesen ueber forSystem
  (zwei Zuweisungen), Schreibzeile je Altzeile ueber forTenant(config.tenantId);
  Tests 301/306 umgedreht, neuer Altzeilen-Test
- tender-digest: Kandidatenabfrage ueber forSystem, Schleife gebunden (+1 Test)
- tender-matching: Profilabfrage ueber forSystem, Katalog (D-03) ungebunden (+1 Test)
- tender-notifications.integration.spec: Mock um forSystem
- Werkzeug: LdapConfig (15 Spalten), LdapFieldMapping (6), TenderMatch (8),
  TenderSavedSearch (8) je neun Kennungen plus Relations-Kennung
  ldapconfig-systemkontext-include-fieldmappings-beider-mandanten
  -> Alle 253 Pruefungen bestanden
- Detektor: FORSYSTEM_ALLOWED_CALL_SITES auf 4 Dateien / 5 Aufrufe;
  Proben-Empfaenger sysPrisma (Gate-Zaehlung, Name nicht hartkodiert)
- Klassifikation: 6 Zeilen system-gebunden, settings/smtpConfig gebunden
- Falsifizierung durch Rueckbau ausgefuehrt und zurueckgenommen:
  (a) FOR SELECT bei TenderMatch entfernt -> 5 von 253 rot (Insert gelingt,
  cmd ALL); (b) Regel TenderSavedSearch aus der Datei entfernt -> 1 von 245
  rot (Extraktion), lebende DB bleibt bei 34; (c) local=false -> gruen, plus
  Reset entfernt -> 5 rot (Erben sichtbar); (d) Zahl 0 -> 2 rot, Fremddatei
  admin-seed -> 3 rot
- Baseline: 64 Dateien / 1054 Tests, tsc 0, Werkzeug 253

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 11:46:08 +02:00
parent 3d645674f0
commit 6e2a641d76
18 changed files with 828 additions and 336 deletions
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { forTenant } from '../prisma/prisma-tenant.extension';
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
import { TenderDigestScheduler } from './tender-digest.scheduler';
/**
@@ -29,6 +29,8 @@ import { TenderDigestScheduler } from './tender-digest.scheduler';
*/
vi.mock('../prisma/prisma-tenant.extension', () => ({
forTenant: vi.fn((prisma: any, tenantId: string) => prisma.__makeBoundClient(tenantId)),
// Systemkontext (260914-eym): die Kandidatenabfrage laeuft ueber forSystem().
forSystem: vi.fn((prisma: any) => prisma.__makeSystemClient()),
}));
const MONDAY = new Date('2026-07-27T10:00:00Z');
@@ -84,6 +86,7 @@ function makeFakePrisma(opts: {
};
const modelsByName: Record<string, any> = { tenderMatch, tenderNotificationPref, user };
const systemCallLog: { model: string; method: string }[] = [];
return {
tenderMatch,
@@ -91,6 +94,19 @@ function makeFakePrisma(opts: {
user,
__store: { matches, prefs, users },
__boundCallLog: boundCallLog,
__systemCallLog: systemCallLog,
// Systemkontext-Klient (260914-eym): protokolliert in __systemCallLog,
// tenderMatch.findMany unveraendert (dieselbe Fake-Implementierung).
__makeSystemClient() {
return {
tenderMatch: {
findMany: async (args: any) => {
systemCallLog.push({ model: 'tenderMatch', method: 'findMany' });
return tenderMatch.findMany(args);
},
},
};
},
__makeBoundClient(tenantId: string) {
const bound: any = {};
for (const [modelName, model] of Object.entries(modelsByName)) {
@@ -380,6 +396,26 @@ describe('TenderDigestScheduler — Bindung an forTenant() (260909-laa)', () =>
expect(forTenant).toHaveBeenCalledTimes(1);
});
it('die Kandidatenabfrage laeuft ueber den System-Klienten: __systemCallLog enthaelt genau tenderMatch.findMany, nichts aus der Schleife (260914-eym)', async () => {
vi.mocked(forSystem).mockClear();
const users = new Map([['user-1', { id: 'user-1', email: 'a@tenant.de', tenantId: 'tenant-1' }]]);
const prefs = new Map([['user-1', { userId: 'user-1', digestInterval: 'daily' }]]);
const matches = [makeMatch({ userId: 'user-1', tenantId: 'tenant-1' })];
const prisma = makeFakePrisma({ matches, prefs, users });
const mail = { sendDigest: vi.fn().mockResolvedValue(true) };
const scheduler = new TenderDigestScheduler(makeSchedulerRegistry() as any, prisma as any, mail as any);
await scheduler.runDigest(TUESDAY);
expect(forSystem).toHaveBeenCalledTimes(1);
expect(forSystem).toHaveBeenCalledWith(prisma);
expect(prisma.__systemCallLog).toEqual([{ model: 'tenderMatch', method: 'findMany' }]);
// Die Schleife (Praeferenz, Treffer, Benutzer, Stempelung) lief gebunden,
// nicht ueber den System-Klienten.
expect(prisma.__boundCallLog.length).toBeGreaterThan(0);
expect(mail.sendDigest).toHaveBeenCalledTimes(1);
});
it('die Zugriffe je Kandidatenzeile binden an den Mandanten DIESER Zeile — Zugriffe innerhalb der Schleife laufen auf dem gebundenen Client', async () => {
const users = new Map([['user-1', { id: 'user-1', email: 'a@tenant.de', tenantId: 'tenant-1' }]]);
const prefs = new Map([['user-1', { userId: 'user-1', digestInterval: 'daily' }]]);