diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a2aee58..0eac2f7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -233,7 +233,7 @@ Decimal phases appear between their surrounding integers in numeric order. **Wave 1** *(blocked on Wave 0 completion)* - [x] 07-02-PLAN.md -- Inbox-access layer: InboxProvider interface, IMAP + Exchange providers, config/vehicle/history DTOs (DKV-01) -- [ ] 07-03-PLAN.md -- Export + delivery + SMTP backend: SettingsModule (SMTP CRUD + test), DkvExportService (xlsx + prune), DkvMailService, MailModule DB-SMTP migration (DKV-04/05, D-06) +- [x] 07-03-PLAN.md -- Export + delivery + SMTP backend: SettingsModule (SMTP CRUD + test), DkvExportService (xlsx + prune), DkvMailService, MailModule DB-SMTP migration (DKV-04/05, D-06) **Wave 2** *(blocked on Wave 1 completion)* @@ -256,4 +256,4 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 | 4. Marketplace & Portal Navigation | 0/4 | Not started | - | | 5. Dashboard & Calendar | 5/5 | Complete | 2026-06-24 | | 6. Desktop Client & CI/CD | 2/3 | In Progress| | -| 7. DKV Fleet Module | 2/6 | In Progress | - | +| 7. DKV Fleet Module | 3/6 | In Progress | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index c351a00..5f5635e 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,15 +3,15 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: Phase 07 Plan 02 complete -last_updated: "2026-06-27T00:10:00.000Z" -last_activity: 2026-06-27 -- Phase 07 Plan 02 completed (DKV inbox-access layer) +stopped_at: Phase 07 Plan 03 complete +last_updated: "2026-06-27T00:20:00.000Z" +last_activity: 2026-06-27 -- Phase 07 Plan 03 completed (DKV export + SMTP settings) progress: total_phases: 7 completed_phases: 5 total_plans: 30 - completed_plans: 24 - percent: 71 + completed_plans: 25 + percent: 83 --- # Project State @@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-18) ## Current Position Phase: 07 (dkv-fleet-module) — EXECUTING -Plan: 3 of 6 -Status: Executing Phase 07 (Plan 02 complete) -Last activity: 2026-06-27 -- Phase 07 Plan 02 completed (DKV inbox-access layer) +Plan: 4 of 6 +Status: Executing Phase 07 (Plan 03 complete) +Last activity: 2026-06-27 -- Phase 07 Plan 03 completed (DKV export + SMTP settings) Progress: [██████████] 100% @@ -60,6 +60,7 @@ Progress: [██████████] 100% | Phase 05-dashboard-calendar PP04 | 5min | 2 tasks | 11 files | | Phase 06-desktop-client-ci-cd P01 | 5min | 2 tasks | 13 files | | Phase 06 P03 | 3min | 3 tasks | 3 files | +| Phase 07-dkv-fleet-module P03 | 5min | 4 tasks | 8 files | ## Accumulated Context @@ -102,6 +103,11 @@ Recent decisions affecting current work: - [07-02]: Max attachment size 25MB enforced in both ImapProvider and ExchangeInboxProvider before buffering (T-07-05) - [07-02]: ExchangeInboxProvider uses WellKnownFolderName.Inbox + FindItems (not FindAppointments — email vs calendar EWS API) - [07-02]: export type {} required for type-only re-exports under isolatedModules TypeScript setting +- [07-03]: SettingsService.getStartupSmtpConfig uses findFirst (tenant-agnostic) for MailModule startup transport +- [07-03]: MailModule forRootAsync factory priority: DB SmtpConfig → MAIL_* env → TESSERA_SMTP_* env → localhost:1025 fallback +- [07-03]: DkvMailService injects SettingsService (not PrismaService directly) to reuse decryption logic +- [07-03]: user-files/ path resolved via path.resolve(__dirname, 4 levels up) from apps/api/dist/dkv/ to monorepo root +- [07-03]: Export prune sorted by mtime ascending (oldest first), delete all beyond last 10 ### Pending Todos @@ -121,6 +127,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-06-27T00:10:00.000Z -Stopped at: Phase 07 Plan 02 complete (DKV inbox-access layer) -Resume file: .planning/phases/07-dkv-fleet-module/07-03-PLAN.md +Last session: 2026-06-27T00:20:00.000Z +Stopped at: Phase 07 Plan 03 complete (DKV export + SMTP settings) +Resume file: .planning/phases/07-dkv-fleet-module/07-04-PLAN.md diff --git a/.planning/phases/07-dkv-fleet-module/07-03-SUMMARY.md b/.planning/phases/07-dkv-fleet-module/07-03-SUMMARY.md new file mode 100644 index 0000000..1411fc1 --- /dev/null +++ b/.planning/phases/07-dkv-fleet-module/07-03-SUMMARY.md @@ -0,0 +1,162 @@ +--- +phase: 07-dkv-fleet-module +plan: 03 +subsystem: dkv-export-smtp-settings +tags: [dkv, smtp, settings, nodemailer, xlsx, sheetjs, encryption, aes-256-gcm, mail-module, d-06] +dependency_graph: + requires: [CalendarModule (CalendarCryptoService), PrismaModule (global), DkvModule (Plan 01 types)] + provides: [SettingsModule, SettingsService, DkvExportService, DkvMailService, migrated MailModule] + affects: + - 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 +tech_stack: + 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_.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" +key_files: + created: + - 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 + modified: + - apps/api/src/mail/mail.module.ts + - apps/api/src/app.module.ts +decisions: + - "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" +metrics: + duration: 5min + completed: "2026-06-27T00:15:00Z" + tasks: 4 + files_created: 6 + files_modified: 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/ → /`. 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)