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:
@@ -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");
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user