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:
@@ -197,6 +197,31 @@ model DkvModuleConfig {
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// Phase 14, Plan 03 (INGEST-05, CONFIG-02, D-06/D-07) — per-tenant portal-
|
||||
// alert mailbox config, mirroring DkvModuleConfig's shape/pattern exactly
|
||||
// (own tenantId @unique row, own encrypted creds — D-03: each module keeps
|
||||
// its own independent mailbox config, this is a SEPARATE mailbox from the
|
||||
// DKV invoice inbox). Credentials are encrypted via CalendarCryptoService
|
||||
// (same AES-256-GCM iv:authTag:ciphertext format as DkvModuleConfig/
|
||||
// SmtpConfig) and excluded from every API response (Safe-Select, T-07-12).
|
||||
model TenderEmailConfig {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @unique
|
||||
protocol String @default("imap") // 'imap' | 'exchange'
|
||||
host String?
|
||||
port Int?
|
||||
encryption String @default("ssl-tls") // 'none' | 'starttls' | 'ssl-tls'
|
||||
folder String @default("INBOX")
|
||||
senderFilter String?
|
||||
domain String? // Exchange only: Windows domain (optional)
|
||||
isActive Boolean @default(false)
|
||||
encryptedInboxCreds String? // AES-256-GCM: JSON { username, password } encrypted
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
model DkvVehicleMaster {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
@@ -285,6 +310,15 @@ model Tender {
|
||||
// for pre-existing rows by backfill-tender-source.ts (Plan 13-01 Task 2).
|
||||
// dedupKey above stays the SCHEMA-02 upsert target — NOT replaced here.
|
||||
fingerprint String?
|
||||
// Phase 14, Plan 03 (INGEST-05, D-13) — per-tenant visibility for PRIVATE
|
||||
// sources only. null = global/platform-wide (D-03, unchanged for all
|
||||
// public sources: DÖE/NetServer/cosinex/RSS — existing rows stay null,
|
||||
// no backfill). Set = visible ONLY to that tenant (email-alert tenders,
|
||||
// since an alert mailbox reflects one tenant's private subscription).
|
||||
// Read filter: buildTenderWhere OR[{ownerTenantId:null},{ownerTenantId:tenant}].
|
||||
// Write: dedup CREATE only sets this — the UPDATE branch never touches it,
|
||||
// so a source later also seen globally is never retroactively hidden.
|
||||
ownerTenantId String?
|
||||
publishedAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -295,7 +329,10 @@ model Tender {
|
||||
@@index([bundesland]) // FILTER-02: post-backfill Bundesland-Filter-Performance
|
||||
@@index([cpvDivisions], type: Gin) // FILTER-03: post-backfill CPV-Divisions-Filter-Performance (hasSome)
|
||||
@@index([fingerprint]) // SCHEMA-03: dedup resolver fingerprint-tier lookup
|
||||
// Deliberately NO tenant column and NO tenant index — this is global data (D-03)
|
||||
@@index([ownerTenantId]) // D-13: read-filter lookup for private (email-alert) tenders
|
||||
// Deliberately NO tenant column/index for the platform-wide default (D-03)
|
||||
// — ownerTenantId above is the sole, additive, nullable exception for
|
||||
// privately-sourced tenders (D-13).
|
||||
triage TenderTriage[]
|
||||
matches TenderMatch[]
|
||||
sources TenderSource[] // SCHEMA-03/D-03 — all source portals this tender was seen on
|
||||
|
||||
Reference in New Issue
Block a user