Files
tessera-ctl/.planning/quick/260909-laa-mandantentrennung-etappe-2-bereich-tende/260909-laa-SUMMARY.md
T

14 KiB
Raw Blame History

phase, plan, subsystem, tags, requires, provides, affects, actuals, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects actuals tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
quick-260909-laa 01 database
prisma
postgresql
row-level-security
multi-tenancy
nestjs
phase provides
quick-260909-jts forTenant()/withTenantTransaction() pattern proven for background-job "beides" cases (groups)
Five tender user-CRUD services (saved-search, triage, notification-pref, email-config, rss-feed) bound to forTenant()
Per-hit halves of both tender background services (digest scheduler, matching service) bound to forTenant()
rls-scratch-check.mjs tenders-area section measuring WINDOWS
docs/mandantentrennung-etappe2-fehlerrichtung.md "Bereich tenders" section (signal table + silent-failure form)
docs/mandantentrennung-zugriffsklassifikation.md fully synced for all 23 tenders pairs
quick-260909-next-tenders-area-or-stage-3
settings-area-quick-task
stage-3-planning
tokens tasks commits
39000 3 3
added patterns
forTenant() bound once per method / once per loop-hit-row, never shared across methods (groups/ldap convention)
__makeBoundClient() two-client test proof (same in-memory Map, per-call logging wrapper) instead of an identity mock
P2002 on a tenant-less unique key (userId, [userId,tenderId]) translated into a German ConflictException — NACHTRAG: bei Lieferung galt das nur fuer die beiden userId-Schluessel; setTriage() auf [userId,tenderId] fehlte und wurde vom Verifizierer gefunden, nachgereicht mit 8cbf4c1 samt zwei Tests, deren Rotwerden durch Rueckbau belegt ist
created modified
apps/api/scripts/rls-scratch-check.mjs
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-zugriffsklassifikation.md
apps/api/src/tenders/tender-saved-search.service.ts
apps/api/src/tenders/tender-triage.service.ts
apps/api/src/tenders/tender-notification-pref.service.ts
apps/api/src/tenders/tender-email-config.service.ts
apps/api/src/tenders/tender-rss-feed.service.ts
apps/api/src/tenders/tenders.controller.ts
apps/api/src/tenders/tender-digest.scheduler.ts
apps/api/src/tenders/tender-matching.service.ts
tender-rss-feed.service.ts reclassified from muss-mandantengebunden to beides (same precedent as ldapConfig in 260909-ipc) — createForUser binds, listForUser/createPlatform/remove stay deliberately unbound (WINDOWS #19)
No withTenantTransaction() introduced in this area — measured exactly one $transaction (array form, platform-global Tender table, tender-fingerprint-backfill.service.ts), outside any tenant binding
tender-digest.scheduler.ts candidate query additionally selects the denormalized tenantId of the match row so the loop can bind; the tenant-switch edge case is named, not solved (Stage 3)
Silent-failure notification form documented separately from the visible-emptiness form in the critique doc, with the notifiedAt-stays-NULL retry guarantee as the one checkable signal
WINDOWS-20
ETAPPE-2-TENDERS
id description verification human_judgment
D1 rls-scratch-check.mjs tenders-area section measures the three special cases (no user dimension in the policies, WINDOWS #19 platform-row invisibility, P2002 on a bound upsert to an invisible row) against the delivered migration
kind ref status
other node apps/api/scripts/rls-scratch-check.mjs — 32/32 checks passed pass
false
id description requirement verification human_judgment
D2 Five user-CRUD services (saved-search, triage, notification-pref, email-config, rss-feed) bound to forTenant(); rss-feed's three intentionally-unbound paths stay unbound with WINDOWS #19 comments WINDOWS-20
kind ref status
unit apps/api/src/tenders/tender-saved-search.service.spec.ts, tender-triage.service.spec.ts, tender-notification-pref.service.spec.ts, tender-email-config.service.spec.ts, tender-rss-feed.service.spec.ts, tenders.controller.spec.ts pass
false
id description requirement verification human_judgment
D3 Per-hit halves of tender-digest.scheduler.ts and tender-matching.service.ts bound to forTenant(); cross-tenant candidate/profile queries stay unbound with a Stage-3-handoff comment ETAPPE-2-TENDERS
kind ref status
unit apps/api/src/tenders/tender-digest.scheduler.spec.ts, tender-matching.service.spec.ts, tender-notifications.integration.spec.ts pass
false
id description verification human_judgment
D4 docs/mandantentrennung-etappe2-fehlerrichtung.md gets a 'Bereich tenders' section with the observed measurement, a per-path signal table, and a dedicated silent-failure-form subsection; docs/mandantentrennung-zugriffsklassifikation.md stays in sync with rls-access-inventory.spec.ts for all 23 tenders pairs
kind ref status
unit apps/api/src/prisma/rls-access-inventory.spec.ts pass
false
45min 2026-09-09 complete

Quick Task 260909-laa: Etappe 2 Bereich tenders Summary

Five tender user-CRUD services and the per-hit halves of two background notification services bound to forTenant(), with the platform-wide catalog, two fan-out adapters, and three WINDOWS-#19-affected RSS paths deliberately left unbound and documented as such.

Performance

  • Duration: ~45 min
  • Started: 2026-09-09T13:20:00Z (approx.)
  • Completed: 2026-09-09T14:03:00Z
  • Tasks: 3
  • Files modified: 20 (13 source/spec files + 2 docs, across three commits)

Accomplishments

  • Measured the three special cases of this area against the delivered _rls_remaining_tenant_tables migration (32/32 scratch checks): the five policies have no user dimension, a platform-wide RSS row is invisible under every tenant context, and a bound upsert onto an invisible cross-tenant row fails on the unique constraint, not the policy.
  • Bound the five per-user CRUD services (tender-saved-search, tender-triage, tender-notification-pref, tender-email-config, tender-rss-feed) to forTenant(); tender-rss-feed.service.ts binds only createForUser and leaves listForUser/createPlatform/remove deliberately unbound with a WINDOWS #19 code comment, because they touch the nullable-tenant platform-wide row.
  • Bound the per-hit halves of both background notification services (tender-digest.scheduler.ts, tender-matching.service.ts) to the tenant of the candidate/profile row currently being processed; their cross-tenant candidate/profile queries stay unbound and are commented as a Stage 3 handoff.
  • Wrote a ## Bereich tenders section into the critique document with the observed scratch-tool output, a per-path signal table, and a dedicated subsection for this area's new failure form: two notification paths that, on too little read, send nothing — silently.
  • Kept docs/mandantentrennung-zugriffsklassifikation.md in sync with rls-access-inventory.spec.ts for all 23 tenders pairs, including reclassifying tenderRssFeedSource from muss-mandantengebunden to beides.

Task Commits

Each task was committed atomically:

  1. Task 1: Measure the three special cases and write the tenders failure-direction section - 3498147 (feat)
  2. Task 2: Bind the five user-CRUD services and thread tenantId through the controller - 3336a6e (feat)
  3. Task 3: Bind the per-hit halves of both background services and close both documents - df5c5b7 (feat)

Plan metadata: committed separately by the orchestrator after this summary.

Note: all three tasks were TDD-flavored (test proof before/alongside the binding change), single commit per task since the two-client proof and the binding change belong to the same logical unit.

Files Created/Modified

  • apps/api/scripts/rls-scratch-check.mjs — new runTendersAreaChecks section (9 checks: no user dimension, WINDOWS #19 invisibility + rejected insert, P2002 on invisible-row upsert), wired into main() after runGroupsAreaChecks
  • docs/mandantentrennung-etappe2-fehlerrichtung.md — new ## Bereich tenders section (t1–t5), plus a Task 3 nachtrag closing the per-hit halves
  • docs/mandantentrennung-zugriffsklassifikation.md — Stand column for 5 pairs updated in Task 2, 5 more in Task 3; area overview table, class distribution, and "Der Hintergrunddienst als Falle" section all re-measured and updated
  • apps/api/src/tenders/tender-saved-search.service.ts — list/update/remove bind via forTenant(), gained tenantId parameter
  • apps/api/src/tenders/tender-triage.service.ts — listForUser/favoriteIds bind via forTenant(), gained tenantId parameter
  • apps/api/src/tenders/tender-notification-pref.service.ts — getForUser binds, setForUser translates P2002 into a German ConflictException
  • apps/api/src/tenders/tender-email-config.service.ts — getConfigForApi/testConnection bind and gained tenantId; saveConfig's internal read now also binds; P2002 translated
  • apps/api/src/tenders/tender-rss-feed.service.ts — createForUser binds; listForUser/createPlatform/remove stay unbound with WINDOWS #19 comments
  • apps/api/src/tenders/tenders.controller.ts — eight call sites thread tenantId from extractTriageContext into the newly-parameterized service methods
  • apps/api/src/tenders/tender-digest.scheduler.ts — candidate query selects denormalized tenantId; per-candidate loop binds pref/match/user/stamp
  • apps/api/src/tenders/tender-matching.service.ts — per-profile loop binds match upsert and the instant-dispatch fresh/user/stamp accesses
  • All corresponding .spec.ts files — __makeBoundClient() two-client proof, per-method binding tests, gegentest for the three intentionally-unbound RSS paths, silent-failure tests for both background services

Decisions Made

  • tender-rss-feed.service.ts/tenderRssFeedSource reclassified from muss-mandantengebunden to beides in the classification doc — mirrors the ldapConfig precedent from 260909-ipc, no behavior change, just a more accurate class.
  • No withTenantTransaction() introduced anywhere in this area: Task 1 measured exactly one $transaction in apps/api/src/tenders (array form, on the platform-global Tender table in tender-fingerprint-backfill.service.ts), outside any tenant binding — the extension header's mandated re-check for a new transactional case is answered with "no new case," not assumed.
  • The digest scheduler's candidate query now additionally selects the match row's denormalized tenantId so the per-row loop can bind at all; the edge case of a user having matches under two different tenants (a stale denormalized value after a tenant switch) is named in code and in both docs, not solved — explicitly Stage 3's problem.

Deviations from Plan

None — plan executed exactly as written. The one place where execution diverged from the plan's literal wording (Befund C's "zehn Aufrufstellen") is a clarification, not a deviation: of the ten controller call sites that today discard tenantId, only eight actually needed the parameter threaded through, because the two RSS call sites (listRssFeeds, removeRssFeed) call service methods (listForUser, remove) that deliberately stay unbound and therefore never gained a tenantId parameter. This is the same "a raw count is a claim, not a finding" lesson the plan itself calls out repeatedly (Befund C is analogous to the 62-vs-61 raw-hit correction) — verified by re-reading each of the ten call sites individually rather than trusting the count.

Issues Encountered

  • The rls-access-inventory.spec.ts doc-vs-source consistency check failed after Task 2's and Task 3's binding changes, as expected since the check runs on every test invocation — the classification doc's Stand column was updated within the same task (not deferred to a later pass) so every task's own verification stayed self-contained and green.
  • TypeScript flagged two implicit-any parameters in tender-matching.service.ts after tenantPrisma (typed any) replaced this.prisma as the receiver for two .map() calls — fixed with explicit inline parameter types (match: { tender: unknown }, match: { id: string }).

User Setup Required

None — no external service configuration required. DATABASE_URL remains on the tessera role with BYPASSRLS; the switch stays off.

Next Phase Readiness

  • The tenders area is now at a mixed-but-fully-documented state: 5 pairs fully bound, 1 pair (tenderRssFeedSource) mixed with a code-level WINDOWS #19 boundary, 2 pairs (tenderMatch/user in the two background services split across beides/gemischt) with their per-hit halves closed and cross-tenant halves named as Stage 3 handoffs, and 12 pairs deliberately untouched (D-03 catalog + fan-out adapters).
  • Stage 3 inherits: the WINDOWS #19 policy fix for TenderRssFeedSource/SearchProvider, the cross-tenant halves of the two background services (with the documented tenant-switch edge case), and the req.tenantPrisma architecture question (still undecided, as in every prior area).
  • Stage 4 (cutover) preflight inherits Befund K: tender-mail.service.ts depends on SettingsService.getDecryptedSmtpConfig(tenantId), and settings.service.ts (4 raw hits) is still fully unbound — after cutover this would silently stop all outbound mail. Recorded in the critique doc, not solved here.
  • The classification doc's area overview now shows tenders at 36 ungebunden / 26 gebunden (was 62/0); remaining areas at their prior stand: dkv (21), user (17), module-registry (17), dashboard (13), calendar (12), tenant (8), favorites (7), settings (4) — all still fully untouched, the largest remaining pool of work for whichever Stage 2 area comes next.

Phase: quick-260909-laa Completed: 2026-09-09

Self-Check: PASSED

All 12 referenced artifact files found on disk; all 3 task commit hashes (3498147, 3336a6e, df5c5b7) found in git history.