feat(14-03): add per-tenant encrypted TenderEmailConfig + ownerTenantId write-side (D-13)

Prisma: new TenderEmailConfig model (per-tenant, tenantId @unique, mirrors
DkvModuleConfig) + Tender.ownerTenantId nullable column + index (D-13:
null = global/platform-wide, unchanged for all existing rows and every
public source; set = visible only to that tenant). Migration
20260723113917_tender_email_config_owner_tenant_id applied locally.

TenderEmailConfigService: safe-select admin CRUD (GET never returns the
password, only hasPassword — T-07-12) with DkvService's encrypt-preserve-
empty semantics, via CalendarCryptoService (AES-256-GCM).

RawTenderRecord/NormalizedTenderFields gain optional ownerTenantId,
threaded through TenderNormalizerService.assemble() unchanged.
TenderDedupService's CREATE branch writes ownerTenantId (defaulting to
null); the UPDATE branch deliberately never references it, so a tender
later also seen on a public source is never retroactively hidden.

EmailAlertAdapter.fetchTenders() now does the real per-tenant fan-out:
findMany({isActive:true}) across ALL tenants (deliberate, documented
cross-tenant platform-scheduler read, never forTenant()/RLS), decrypts
each tenant's credentials, picks imap/exchange provider, and tags every
extracted candidate with ownerTenantId — catch-per-tenant so one broken
mailbox never blocks the others.

tenders.module.ts: imports CalendarModule/InboxModule, registers
EmailAlertAdapter + TenderEmailConfigService, seeds an 'email-alert'
TenderSourcePollConfig row (pollGranularity='tick', isActive=false —
no default mailbox to activate yet, D-02 framework-ready stance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:45:11 +02:00
parent 8983231196
commit 1be6b15249
12 changed files with 994 additions and 21 deletions
@@ -0,0 +1,30 @@
-- AlterTable
ALTER TABLE "Tender" ADD COLUMN "ownerTenantId" TEXT;
-- CreateTable
CREATE TABLE "TenderEmailConfig" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"protocol" TEXT NOT NULL DEFAULT 'imap',
"host" TEXT,
"port" INTEGER,
"encryption" TEXT NOT NULL DEFAULT 'ssl-tls',
"folder" TEXT NOT NULL DEFAULT 'INBOX',
"senderFilter" TEXT,
"domain" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT false,
"encryptedInboxCreds" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TenderEmailConfig_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "TenderEmailConfig_tenantId_key" ON "TenderEmailConfig"("tenantId");
-- CreateIndex
CREATE INDEX "TenderEmailConfig_tenantId_idx" ON "TenderEmailConfig"("tenantId");
-- CreateIndex
CREATE INDEX "Tender_ownerTenantId_idx" ON "Tender"("ownerTenantId");