feat(12-01): add TenderMatch/TenderNotificationPref schema + apply migration
- TenderMatch: one row per (tenderId, savedSearchId) pair, single nullable notifiedAt as the matched-vs-notified eligibility gate (D-06) - TenderNotificationPref: per-user digest interval (daily/weekly/off, D-01/D-03) - TenderSavedSearch.instantAlert: per-profile instant alert flag, default off (D-04) - Migration 20260722100000_add_tender_notifications applied to local dev DB (docker exec psql), recorded in _prisma_migrations, prisma generate run Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
-- AlterTable
|
||||
-- NOTIFY-02 (D-04): Sofort-Alert pro Profil, Default AUS.
|
||||
ALTER TABLE "TenderSavedSearch" ADD COLUMN IF NOT EXISTS "instantAlert" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- CreateTable
|
||||
-- NOTIFY-03 Kern-Invariante — der "getroffen"-Datensatz (D-06). Ein Row je
|
||||
-- (tender x savedSearch)-Paar. EIN einziges "notifiedAt" ist das
|
||||
-- Eligibility-Gate: NULL = noch nicht benachrichtigt, gesetzt (instant
|
||||
-- ODER digest) = das Paar wird NIE wieder benachrichtigt. Match-Erzeugung
|
||||
-- ist ein idempotenter Upsert auf @@unique(tenderId, savedSearchId) mit
|
||||
-- update:{} — ein Re-Match bewahrt ein bereits gesetztes notifiedAt.
|
||||
-- Scoping wie TenderTriage/TenderSavedSearch: userId ist das Scoping-Feld
|
||||
-- fuer Reads (where:{userId}, IDOR-Schutz), tenantId zusaetzlich fuer die
|
||||
-- Mandanten-SMTP-Auflösung im Digest/Instant-Versand — KEIN forTenant()/RLS.
|
||||
CREATE TABLE IF NOT EXISTS "TenderMatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenderId" TEXT NOT NULL,
|
||||
"savedSearchId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"matchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"notifiedAt" TIMESTAMP(3),
|
||||
"notifiedChannel" TEXT,
|
||||
|
||||
CONSTRAINT "TenderMatch_pkey" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "TenderMatch_tenderId_fkey" FOREIGN KEY ("tenderId") REFERENCES "Tender"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "TenderMatch_savedSearchId_fkey" FOREIGN KEY ("savedSearchId") REFERENCES "TenderSavedSearch"("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
-- NOTIFY-01 (D-03) — Digest-Intervall ist eine per-USER Einstellung, nicht
|
||||
-- pro Profil. Default 'daily' (D-01).
|
||||
CREATE TABLE IF NOT EXISTS "TenderNotificationPref" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"digestInterval" TEXT NOT NULL DEFAULT 'daily',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TenderNotificationPref_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
-- Upsert-Target — ein Match je (tender x savedSearch)-Paar, idempotent.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "TenderMatch_tenderId_savedSearchId_key" ON "TenderMatch"("tenderId", "savedSearchId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "TenderMatch_userId_idx" ON "TenderMatch"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "TenderMatch_notifiedAt_idx" ON "TenderMatch"("notifiedAt");
|
||||
|
||||
-- CreateIndex
|
||||
-- Ein Pref-Row je Nutzer.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "TenderNotificationPref_userId_key" ON "TenderNotificationPref"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX IF NOT EXISTS "TenderNotificationPref_userId_idx" ON "TenderNotificationPref"("userId");
|
||||
Reference in New Issue
Block a user