test(11-05): add failing TenderTriageService spec + TenderTriage schema/migration

RED phase (TDD) for UI-03/04 per-user triage (gelesen/ungelesen, Favorit).
Adds the TenderTriage Prisma model (userId-scoped, onDelete: Cascade to
Tender per Pitfall 6) and its migration, plus a failing spec proving
upsert idempotency, strict userId scoping (V4/IDOR, T-11-10), and cascade
consistency — service implementation follows in the GREEN commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:28:38 +02:00
parent 4c22d7731a
commit 7bb695d37a
3 changed files with 203 additions and 0 deletions
@@ -0,0 +1,32 @@
-- CreateTable
-- UI-03/04 (D-09/D-10/D-11) — per-user Triage-Zustand (gelesen/ungelesen,
-- Favorit) pro Tender. Scoping-Muster wie FavoriteLink (T-08-06, Pitfall 4):
-- userId-Scoping im Service, KEIN forTenant()/RLS. tenantId wird zusätzlich
-- mitgeführt (T-11-12, spätere Tenant-Isolation), ist aber nicht das
-- Scoping-Feld. ON DELETE CASCADE stellt sicher, dass Retention-Löschungen
-- (Phase 10, 90 Tage) keine verwaisten Triage-Zeilen hinterlassen (Pitfall 6).
CREATE TABLE IF NOT EXISTS "TenderTriage" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"tenderId" TEXT NOT NULL,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"isFavorite" BOOLEAN NOT NULL DEFAULT false,
"readAt" TIMESTAMP(3),
"favoritedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TenderTriage_pkey" PRIMARY KEY ("id"),
CONSTRAINT "TenderTriage_tenderId_fkey" FOREIGN KEY ("tenderId") REFERENCES "Tender"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
-- Upsert-Target für setTriage (@@unique([userId,tenderId])).
CREATE UNIQUE INDEX IF NOT EXISTS "TenderTriage_userId_tenderId_key" ON "TenderTriage"("userId", "tenderId");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "TenderTriage_userId_idx" ON "TenderTriage"("userId");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "TenderTriage_tenderId_idx" ON "TenderTriage"("tenderId");
+24
View File
@@ -290,6 +290,30 @@ model Tender {
@@index([bundesland]) // FILTER-02: post-backfill Bundesland-Filter-Performance
@@index([cpvDivisions], type: Gin) // FILTER-03: post-backfill CPV-Divisions-Filter-Performance (hasSome)
// Deliberately NO tenant column and NO tenant index — this is global data (D-03)
triage TenderTriage[]
}
// UI-03/04 — per-user Triage-Zustand pro Tender (gelesen/ungelesen, Favorit).
// Scoping-Muster wie FavoriteLink (T-08-06, Pitfall 4): userId-Scoping im
// Service, KEIN forTenant()/RLS — RLS existiert nur für Auth-Kerntabellen.
// tenantId wird zusätzlich mitgeführt (spätere Tenant-Isolation, T-11-12),
// ist aber NICHT das Scoping-Feld — jede Query filtert auf userId.
model TenderTriage {
id String @id @default(uuid())
userId String
tenantId String
tenderId String
isRead Boolean @default(false)
isFavorite Boolean @default(false)
readAt DateTime?
favoritedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tender Tender @relation(fields: [tenderId], references: [id], onDelete: Cascade)
@@unique([userId, tenderId]) // Upsert-Target (setTriage); ein Triage-Row je (user,tender)
@@index([userId])
@@index([tenderId])
}
// Singleton-per-source admin poll config (INGEST-06 foundation).