feat(10-01): add Tender/TenderSourcePollConfig models + ingestion packages

- Install fast-xml-parser, adm-zip, csv-parse in @tessera/api
- Add global Tender model (no tenantId — D-03 platform-global data)
- Add singleton TenderSourcePollConfig (sourceType @unique)
- Handwritten additive migration for both tables
- Regenerate Prisma client
This commit is contained in:
2026-07-21 10:33:24 +02:00
parent b33213b04b
commit 713b1eb748
4 changed files with 175 additions and 0 deletions
@@ -0,0 +1,54 @@
-- CreateTable
CREATE TABLE IF NOT EXISTS "Tender" (
"id" TEXT NOT NULL,
"sourcePortal" TEXT NOT NULL,
"sourceNoticeId" TEXT NOT NULL,
"ocid" TEXT,
"dedupKey" TEXT NOT NULL,
"title" TEXT NOT NULL,
"buyerName" TEXT,
"cpvCodes" TEXT[] DEFAULT ARRAY[]::TEXT[],
"region" TEXT,
"plz" TEXT,
"bundesland" TEXT,
"deadlineAt" TIMESTAMP(3),
"estimatedValue" DECIMAL(14,2),
"procedureType" TEXT,
"status" TEXT NOT NULL DEFAULT 'active',
"sourceUrl" TEXT,
"contentHash" TEXT NOT NULL,
"rawPayload" JSONB,
"publishedAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Tender_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE IF NOT EXISTS "TenderSourcePollConfig" (
"id" TEXT NOT NULL,
"sourceType" TEXT NOT NULL,
"pollIntervalMin" INTEGER NOT NULL DEFAULT 60,
"isActive" BOOLEAN NOT NULL DEFAULT false,
"lastIngestedDay" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TenderSourcePollConfig_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "Tender_dedupKey_key" ON "Tender"("dedupKey");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "Tender_status_idx" ON "Tender"("status");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "Tender_deadlineAt_idx" ON "Tender"("deadlineAt");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "Tender_publishedAt_idx" ON "Tender"("publishedAt");
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "TenderSourcePollConfig_sourceType_key" ON "TenderSourcePollConfig"("sourceType");
+43
View File
@@ -256,3 +256,46 @@ model FavoriteLink {
@@index([tenantId])
@@index([widgetId])
}
// Ausschreibungs-Radar (Phase 10) — platform-global tender reference data.
// D-03: Tender carries NO tenantId and is NOT wrapped by forTenant()/RLS —
// per-tenant scoping (saved searches) lives one layer up in Phase 11.
model Tender {
id String @id @default(uuid())
sourcePortal String // e.g. 'doe-opendata'
sourceNoticeId String
ocid String? // OCDS contracting id, when present
dedupKey String @unique // ocid, or fallback sourcePortal:sourceNoticeId — SCHEMA-02 upsert target
title String
buyerName String?
cpvCodes String[] @default([])
region String?
plz String?
bundesland String?
deadlineAt DateTime? // frequently null (RESEARCH Pattern 4) — nullable is mandatory
estimatedValue Decimal? @db.Decimal(14, 2)
procedureType String?
status String @default("active") // 'active' | 'expired' (D-05 retention marking)
sourceUrl String?
contentHash String // SCHEMA-02 change detection
rawPayload Json? // debugging / re-normalization
publishedAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([deadlineAt])
@@index([publishedAt])
// Deliberately NO tenant column and NO tenant index — this is global data (D-03)
}
// Singleton-per-source admin poll config (INGEST-06 foundation).
model TenderSourcePollConfig {
id String @id @default(uuid())
sourceType String @unique // fixed slug 'doe-opendata' — @unique makes singleton intent explicit
pollIntervalMin Int @default(60) // D-04 default hourly
isActive Boolean @default(false)
lastIngestedDay DateTime? // day-cursor, NOT a timestamp (RESEARCH Pattern 1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}