feat(laa-03): binde die Je-Treffer-Haelften der Hintergrunddienste und schliesse die Dokumente
tender-digest.scheduler.ts: die uebergreifende Kandidatenabfrage (tenderMatch.findMany mit distinct:['userId']) bleibt bewusst ungebunden und waehlt zusaetzlich das denormalisierte tenantId der Treffer-Zeile mit aus; innerhalb der Schleife binden tenderNotificationPref.findUnique, tenderMatch.findMany/updateMany und user.findUnique an den Mandanten DIESER Kandidatenzeile. tender-matching.service.ts: die Profilabfrage (tenderSavedSearch.findMany) und der Lesezugriff auf den plattformweiten Tender-Katalog (D-03) bleiben ungebunden; innerhalb der Profilschleife bindet die Treffer-Anlage (tenderMatch.upsert), im nachgelagerten Instant-Dispatch binden tenderMatch.findMany/updateMany und user.findUnique — je EIN gebundener Client pro Profil, nicht neu je Treffer. Beide Dateien tragen Codekommentare, die die uebergreifenden Abfragen ausdruecklich als Etappe-3-Uebergabe benennen — die Trennlinie zwischen Etappe 2 und Etappe 3 wird hier gezogen, nicht verwischt. Alle drei betroffenen Testdateien (inkl. der gemeinsamen Integrationsdatei) bekommen den Zwei-Client-Nachweis, Tests fuer Zwei-Mandanten-Laeufe und die lautlose Fehlerform (kein Versand, notifiedAt bleibt NULL). Falsifiziert: ein probeweiser Rueckbau der user.findUnique-Bindung im Instant-Dispatch von tender-matching.service.ts machte genau den erwarteten Test rot, danach zurueckgenommen. rls-access-inventory.spec.ts gemessen und docs/mandantentrennung-zugriffsklassifikation.md nachgezogen: Stand der fuenf betroffenen Paare (tender-digest.scheduler.ts/ tenderMatch,tenderNotificationPref,user; tender-matching.service.ts/ tenderMatch,user) auf gemischt bzw. gebunden; Bereichsuebersicht und Klassen-Verteilung neu gemessen (tenders jetzt 36 ungebunden/26 gebunden); "Der Hintergrunddienst als Falle" um den Abschluss beider Dateien ergaenzt. docs/mandantentrennung-etappe2-fehlerrichtung.md: (t4) um den Nachtrag ergaenzt, dass die Je-Treffer-Haelften geschlossen sind und die uebergreifenden Haelften an Etappe 3 uebergeben bleiben. 770 Tests gruen, Typpruefung sauber, Wegwerf-Werkzeug 32/32, kein Schema-/Migrations-/Compose-/Umgebungsdatei-Diff. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
import { TenderMailItem, TenderMailService } from './tender-mail.service';
|
||||
|
||||
/**
|
||||
@@ -104,9 +105,21 @@ export class TenderDigestScheduler implements OnModuleInit {
|
||||
async runDigest(now: Date = new Date()): Promise<void> {
|
||||
// Candidate users: distinct userId with at least one un-notified match,
|
||||
// across ALL tenants — a single findMany, never a per-tenant iteration.
|
||||
// BEWUSST UNGEBUNDEN (260909-laa, Aufgabe 3) — der bewusste Fan-out
|
||||
// über alle Mandanten dieses Bereichs; Etappe-3-Uebergabe (Systemkontext
|
||||
// für Hintergrundläufe wird dort entschieden, hier NICHT vorweggenommen).
|
||||
//
|
||||
// Zusaetzlich das denormalisierte tenantId der Treffer-Zeile mit
|
||||
// ausgewaehlt (nicht Teil von `distinct`), damit die Schleife unten
|
||||
// ueberhaupt an einen Mandanten binden KANN. Sonderfall, NICHT geloest:
|
||||
// ein Nutzer koennte Treffer unter zwei verschiedenen Mandanten haben
|
||||
// (der denormalisierte Wert kann bei einem Mandantenwechsel veralten) —
|
||||
// `distinct(['userId'])` liefert dann nur EINE der moeglichen
|
||||
// tenantId-Werte je Nutzer, welche ist von der internen Zeilenreihenfolge
|
||||
// abhaengig. Siehe docs/mandantentrennung-etappe2-fehlerrichtung.md.
|
||||
const candidates = await this.prisma.tenderMatch.findMany({
|
||||
where: { notifiedAt: null },
|
||||
select: { userId: true },
|
||||
select: { userId: true, tenantId: true },
|
||||
distinct: ['userId'],
|
||||
});
|
||||
|
||||
@@ -114,9 +127,14 @@ export class TenderDigestScheduler implements OnModuleInit {
|
||||
|
||||
const weeklyDue = isMondayInBerlin(now);
|
||||
|
||||
for (const { userId } of candidates) {
|
||||
for (const { userId, tenantId } of candidates) {
|
||||
try {
|
||||
const pref = await this.prisma.tenderNotificationPref.findUnique({
|
||||
// Je-Treffer-Haelfte, gebunden an den Mandanten DIESER
|
||||
// Kandidatenzeile (260909-laa, Aufgabe 3) — ein einziger gebundener
|
||||
// Client fuer alle Zugriffe dieses Schleifendurchlaufs.
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
|
||||
const pref = await tenantPrisma.tenderNotificationPref.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
// Missing pref row -> daily default (D-01).
|
||||
@@ -126,14 +144,14 @@ export class TenderDigestScheduler implements OnModuleInit {
|
||||
if (interval === 'weekly' && !weeklyDue) continue;
|
||||
// 'daily' (or any unrecognized value) -> due every run.
|
||||
|
||||
const matches = await this.prisma.tenderMatch.findMany({
|
||||
const matches = await tenantPrisma.tenderMatch.findMany({
|
||||
where: { userId, notifiedAt: null },
|
||||
include: { tender: true, savedSearch: true },
|
||||
orderBy: { savedSearch: { name: 'asc' } },
|
||||
});
|
||||
if (!matches.length) continue;
|
||||
|
||||
const user = await this.prisma.user.findUnique({ where: { id: userId } });
|
||||
const user = await tenantPrisma.user.findUnique({ where: { id: userId } });
|
||||
// Kein Konto, oder ein Konto ohne Adresse (WINDOWS #15, kollidierte
|
||||
// AD-Adresse) -- die Zugehoerigkeit funktioniert, nur der
|
||||
// Mailversand wird uebersprungen (zugesagtes Verhalten).
|
||||
@@ -143,7 +161,7 @@ export class TenderDigestScheduler implements OnModuleInit {
|
||||
const sent = await this.mail.sendDigest({ email: user.email }, user.tenantId, sections);
|
||||
|
||||
if (sent) {
|
||||
await this.prisma.tenderMatch.updateMany({
|
||||
await tenantPrisma.tenderMatch.updateMany({
|
||||
where: { id: { in: matches.map((m: { id: string }) => m.id) } },
|
||||
data: { notifiedAt: new Date(), notifiedChannel: 'digest' },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user