Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-03-SUMMARY.md
T
schalli 4ed1889c8d
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Waiting to run
docs(07-03): complete DKV export + SMTP settings plan
- 07-03-SUMMARY.md: all 4 tasks documented, threat mitigations verified,
  user-files/ path resolution and MailModule factory priority chain explained
- STATE.md: advanced to Plan 4 of 6, added 5 new decisions, metrics row
- ROADMAP.md: 07-03 checked complete, Phase 7 progress 3/6
2026-06-27 00:16:05 +02:00

9.8 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
07-dkv-fleet-module 03 dkv-export-smtp-settings
dkv
smtp
settings
nodemailer
xlsx
sheetjs
encryption
aes-256-gcm
mail-module
d-06
requires provides affects
CalendarModule (CalendarCryptoService)
PrismaModule (global)
DkvModule (Plan 01 types)
SettingsModule
SettingsService
DkvExportService
DkvMailService
migrated MailModule
apps/api/src/settings/settings.module.ts
apps/api/src/settings/settings.controller.ts
apps/api/src/settings/settings.service.ts
apps/api/src/settings/dto/smtp-config.dto.ts
apps/api/src/dkv/dkv-export.service.ts
apps/api/src/dkv/dkv-mail.service.ts
apps/api/src/mail/mail.module.ts
apps/api/src/app.module.ts
added patterns
SettingsService.getSmtpConfig: SMTP_SAFE_SELECT excludes encryptedPassword; GET returns hasPassword boolean (T-07-07)
SettingsService.saveSmtpConfig: CalendarCryptoService.encrypt() for AES-256-GCM password; empty password preserves existing (T-07-08)
SettingsService.testSmtpConfig: nodemailer.createTransport() + transport.verify(); returns only boolean (T-07-16)
SettingsService.getStartupSmtpConfig: findFirst() for tenant-agnostic MailModule factory (D-06); never logs decrypted password (T-07-11)
DkvExportService.buildExcelBuffer: XLSX.utils.aoa_to_sheet with 5-column header; Lieferdatum written as string (anti-pattern avoidance)
DkvExportService.writeAndPrune: server-side filename DKV_YYYY-MM_<nr>.xlsx (T-07-09); prune by mtime ascending; keep last 10 (D-15)
DkvMailService.sendExportEmail: createTransport() at send time from DB (Pitfall 3); error rethrows for D-16 retry (does not swallow)
MailModule.forRootAsync async factory: DB SmtpConfig priority 1; MAIL_HOST/PORT/USER/PASS priority 2; TESSERA_SMTP_* priority 3; localhost:1025 final fallback
created modified
apps/api/src/settings/dto/smtp-config.dto.ts
apps/api/src/settings/settings.service.ts
apps/api/src/settings/settings.controller.ts
apps/api/src/settings/settings.module.ts
apps/api/src/dkv/dkv-export.service.ts
apps/api/src/dkv/dkv-mail.service.ts
apps/api/src/mail/mail.module.ts
apps/api/src/app.module.ts
SettingsService exports getStartupSmtpConfig (findFirst) for MailModule — single-tenant deployments use the one SmtpConfig row as the system mail transport
user-files/ path resolved via path.resolve(__dirname, '..', '..', '..', '..', 'user-files') from apps/api/dist/ — reaches monorepo root regardless of CWD
Prune ordering: mtime ascending (oldest mtime first), slice to delete oldest entries beyond 10-file limit
MailModule forRootAsync factory chain: DB SmtpConfig → MAIL_* env → TESSERA_SMTP_* env → localhost:1025 hardcoded (backward compat preserved)
DkvMailService injects SettingsService (not PrismaService + CalendarCryptoService directly) — avoids duplicate decryption logic
SettingsModule imports CalendarModule (not re-provides CalendarCryptoService) — single source of truth for AES key lifecycle
duration completed tasks files_created files_modified
5min 2026-06-27T00:15:00Z 4 6 2

Phase 07 Plan 03: DKV Export + SMTP Settings — Summary

SettingsModule (SMTP config backend), DkvExportService (xlsx generation + 10-file prune), DkvMailService (runtime nodemailer transport per send), and migrated MailModule (DB SmtpConfig priority with env fallback — D-06).

Tasks Completed

Task Name Commit Key Files
1 SettingsModule — SMTP config backend + connection test (DKV-05) 1bec0e7 settings.module.ts, settings.controller.ts, settings.service.ts, dto/smtp-config.dto.ts, app.module.ts
2 DkvExportService — xlsx generation + user-files/ prune (DKV-04) 6b76ca9 dkv-export.service.ts
3 DkvMailService — runtime SMTP transport with attachment (DKV-04) 4deefb5 dkv-mail.service.ts
4 Migrate MailModule to DB-sourced SMTP transport with env fallback (D-06) de48e35 mail.module.ts

Architecture Notes

user-files/ Path Resolution

DkvExportService resolves the path using:

path.resolve(__dirname, '..', '..', '..', '..', 'user-files')

__dirname at runtime is apps/api/dist/dkv/. Going up 4 levels: dkv/ → dist/ → api/ → apps/ → <repo-root>/. This reaches user-files/ at the monorepo root regardless of the process working directory. The path is never derived from request input (T-07-09).

Export File Prune Ordering

Files matching DKV_*.xlsx in user-files/ are collected, sorted by mtime ascending (oldest modification time first), and all entries beyond index 9 (i.e., beyond the last 10) are deleted via fs.unlinkSync. The writeAndPrune method handles write + prune atomically within a single synchronous call — the caller's processing lock (Plan 04) prevents interleaved operations (Pitfall 7).

MailModule Factory Transport Resolution (D-06)

The MailerModule.forRootAsync factory is async and resolves the transport in this priority order:

  1. DB SmtpConfig (settingsService.getStartupSmtpConfig() → prisma.smtpConfig.findFirst()): used when any row exists. In single-tenant deployments, the one SmtpConfig row serves as the system mail transport. Decrypted password is used only to build the transport object — never logged (T-07-11).
  2. Env vars MAIL_HOST / MAIL_PORT / MAIL_USER / MAIL_PASS: used when no DB row exists.
  3. Legacy env vars TESSERA_SMTP_HOST / TESSERA_SMTP_PORT / TESSERA_SMTP_USER / TESSERA_SMTP_PASSWORD: secondary fallback preserving backward compatibility.
  4. Hardcoded localhost:1025: final fallback for local dev (Mailhog).

No circular import exists: MailModule → SettingsModule → CalendarModule — no reverse edges.

DkvMailService Transport Strategy

Transport is created via nodemailer.createTransport() inside sendExportEmail(), not at module init. This means:

  • SMTP config changes in the admin UI take effect on the next send without a service restart.
  • Contrast with MailerModule/MailService which configure transport once at startup.
  • This is the mandatory pattern for DKV mail per Research Pitfall 3.

Verification Results

  • pnpm --filter @tessera/api type-check exits 0: PASS
  • SettingsModule in app.module.ts imports: PASS
  • SettingsModule exports SettingsService: PASS
  • All controller handlers carry @Roles(Role.ADMIN, Role.SUPER_ADMIN): PASS
  • getSmtpConfig present in settings.service.ts: PASS
  • testSmtpConfig present in settings.service.ts: PASS
  • getStartupSmtpConfig present in settings.service.ts: PASS
  • getStartupSmtpConfig called in mail.module.ts: PASS
  • forRootAsync in mail.module.ts: PASS
  • MAIL_HOST env fallback in mail.module.ts: PASS
  • SettingsModule imported in mail.module.ts: PASS
  • aoa_to_sheet in dkv-export.service.ts: PASS
  • Lieferdatum header in dkv-export.service.ts: PASS
  • 5-column header order exactly matching D-13: PASS
  • MAX_EXPORT_FILES = 10 prune limit: PASS
  • Lieferdatum written as string (no Date wrapping in code): PASS
  • createTransport in dkv-mail.service.ts: PASS
  • No MailerService import in dkv-mail.service.ts: PASS

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] "MailerService" string in Task 3 grep verification

  • Found during: Task 3 verification
  • Issue: The plan's automated check ! grep -q "MailerService" apps/api/src/dkv/dkv-mail.service.ts failed because a JSDoc comment contained the string "This service does NOT import or use MailerService from @nestjs-modules/mailer." The grep test is literal string matching and does not distinguish comments from code.
  • Fix: Replaced the comment wording to "This service uses nodemailer directly — NOT the @nestjs-modules/mailer abstraction." — no MailerService string in the file.
  • Files modified: apps/api/src/dkv/dkv-mail.service.ts
  • Commit: 4deefb5

Known Stubs

None — all functionality is fully implemented. DkvExportService, DkvMailService, and SettingsService contain no placeholder values, hardcoded empty returns, or TODO items that would block Plan 04 orchestration.

Threat Flags

None. All STRIDE threats in this plan's threat register were mitigated:

  • T-07-07: SMTP_SAFE_SELECT excludes encryptedPassword; GET returns hasPassword boolean; encryptedPassword stripped in controller before returning
  • T-07-08: All three endpoints (GET, PUT, POST test) carry @Roles(Role.ADMIN, Role.SUPER_ADMIN); SmtpConfigDto validates host/port/encryption
  • T-07-09: writeAndPrune filename built from invoiceMonth + rechnungsnummer parameters (from parsed PDF), never from HTTP request input
  • T-07-10: DkvMailService catch block logs only (error as Error).message — no credentials, host, transport details
  • T-07-11: getStartupSmtpConfig decrypts password only to build the transport return object; no logging of the value
  • T-07-16: testSmtpConfig returns { success: boolean } only; verify() failures logged with generic message (no credentials)

Self-Check: PASSED

Files verified present:

  • apps/api/src/settings/dto/smtp-config.dto.ts ✓
  • apps/api/src/settings/settings.service.ts ✓
  • apps/api/src/settings/settings.controller.ts ✓
  • apps/api/src/settings/settings.module.ts ✓
  • apps/api/src/dkv/dkv-export.service.ts ✓
  • apps/api/src/dkv/dkv-mail.service.ts ✓
  • apps/api/src/mail/mail.module.ts (modified) ✓
  • apps/api/src/app.module.ts (modified) ✓

Commits verified in git log:

  • 1bec0e7 ✓ (feat(07-03): SettingsModule — SMTP config backend + connection test)
  • 6b76ca9 ✓ (feat(07-03): DkvExportService — xlsx generation + user-files/ prune)
  • 4deefb5 ✓ (feat(07-03): DkvMailService — runtime nodemailer transport with xlsx attachment)
  • de48e35 ✓ (feat(07-03): Migrate MailModule to DB-sourced SMTP transport with env fallback)