docs(07-04): complete DKV integration plane plan
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run

SUMMARY: DkvService pipeline, DkvSchedulerService dynamic cron, DkvController 12 routes,
DkvModule registry seed, AppModule DkvModule registration, dkvFleet + settings i18n keys.
Deviation: CronJob via require() workaround (pnpm transitive dep isolation).
This commit is contained in:
2026-06-27 00:32:19 +02:00
parent 2a3d1c1e85
commit 316f8353b7
2 changed files with 185 additions and 9 deletions
+14 -9
View File
@@ -3,9 +3,9 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
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)
stopped_at: Phase 07 Plan 04 complete
last_updated: "2026-06-26T22:30:00.000Z"
last_activity: 2026-06-26 -- Phase 07 Plan 04 completed (DKV integration plane + REST + i18n)
progress:
total_phases: 7
completed_phases: 5
@@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-18)
## Current Position
Phase: 07 (dkv-fleet-module) — EXECUTING
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)
Plan: 5 of 6
Status: Executing Phase 07 (Plan 04 complete)
Last activity: 2026-06-26 -- Phase 07 Plan 04 completed (DKV integration plane + REST + i18n)
Progress: [██████████] 100%
@@ -61,6 +61,7 @@ Progress: [██████████] 100%
| 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 |
| Phase 07-dkv-fleet-module P04 | 7min | 3 tasks | 8 files |
## Accumulated Context
@@ -108,6 +109,10 @@ Recent decisions affecting current work:
- [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
- [07-04]: DkvScheduler v1 uses findFirst() — single-tenant; multi-tenant scheduling deferred
- [07-04]: Circular dep DkvService<->DkvScheduler avoided via controller coordination after PUT config
- [07-04]: CronJob resolved via require() workaround (pnpm strict isolation: transitive dep)
- [07-04]: rechnungsnummer from email subject regex /d{2}-d{9}-d{3}/; fallback=email-{uid}
### Pending Todos
@@ -127,6 +132,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity
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
Last session: 2026-06-26T22:30:00.000Z
Stopped at: Phase 07 Plan 04 complete (DKV integration plane)
Resume file: .planning/phases/07-dkv-fleet-module/07-05-PLAN.md
@@ -0,0 +1,171 @@
---
phase: 07-dkv-fleet-module
plan: 04
subsystem: dkv-integration-plane
tags: [dkv, nestjs, scheduler, cron, pipeline, orchestration, rest, i18n, module-registry]
dependency_graph:
requires: [DkvParserService (07-01), ImapProvider + ExchangeInboxProvider (07-02), DkvExportService + DkvMailService + SettingsModule (07-03), CalendarModule (CalendarCryptoService)]
provides: [DkvService, DkvSchedulerService, DkvController, DkvModule, dkv.seed.ts, dkvFleet i18n namespace, settings.categorySmtp i18n keys]
affects:
- apps/api/src/dkv/dkv.service.ts
- apps/api/src/dkv/dkv-scheduler.service.ts
- apps/api/src/dkv/dkv.controller.ts
- apps/api/src/dkv/dkv.seed.ts
- apps/api/src/dkv/dkv.module.ts
- apps/api/src/app.module.ts
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
tech_stack:
added: []
patterns:
- "Single-flight guard: private processing=false in DkvService.processInbox (Pitfall 7)"
- "D-10 parse retry: 3 attempts, on final failure record Fehler history row with errorMessage"
- "D-16 SMTP retry: 3 attempts, exponential backoff 2s/4s, on final failure record Versand fehlgeschlagen"
- "CONFIG_SAFE_SELECT const excludes encryptedInboxCreds from all API responses (T-07-12)"
- "getExportFile whitelist regex ^DKV_[\\w\\-]+\\.xlsx$ blocks path traversal (T-07-09)"
- "CronJob via require() workaround: cron is transitive dep of @nestjs/schedule under pnpm strict isolation"
- "DkvController coordinates scheduler after PUT /dkv/config (no circular DkvService <-> DkvScheduler dep)"
- "DkvModule imports CalendarModule to get CalendarCryptoService without re-declaring it"
- "Invoice number extracted from email subject via regex /\\d{2}-\\d{9}-\\d{3}/; falls back to email-{uid}"
key_files:
created:
- apps/api/src/dkv/dkv.service.ts
- apps/api/src/dkv/dkv-scheduler.service.ts
- apps/api/src/dkv/dkv.controller.ts
- apps/api/src/dkv/dkv.seed.ts
- apps/api/src/dkv/dkv.module.ts
modified:
- apps/api/src/app.module.ts
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
decisions:
- "DkvScheduler v1: findFirst() loads first active config (single-tenant); full per-tenant scheduling deferred"
- "Circular dep avoided: DkvController injects both DkvService + DkvSchedulerService; DkvService does NOT inject scheduler"
- "rechnungsnummer: extracted from email subject via /\\d{2}-\\d{9}-\\d{3}/ regex; fallback=email-{uid}"
- "invoiceMonth: derived from first lieferdatum (DD.MM.YYYY→YYYY-MM); fallback=current month"
- "Unknown plates export with empty Fahrer + resolveFahrzeug applied with empty marke/modell (consistent with format string)"
- "cron package imported via require() not import: pnpm strict isolation prevents direct import of transitive deps"
- "DkvModule imports CalendarModule (exported CalendarCryptoService) not re-declared in providers"
metrics:
duration: 7min
completed: "2026-06-26T22:30:00Z"
tasks: 3
files_created: 5
files_modified: 3
---
# Phase 07 Plan 04: DKV Integration Plane — Pipeline + REST + Module Summary
DkvService orchestrates the full poll→parse→map→export→send→history pipeline with single-flight guard, 3-retry parse (D-10), and 3-retry SMTP with exponential backoff (D-16). DkvSchedulerService drives configurable polling via SchedulerRegistry dynamic cron. DkvController exposes all 12 ADMIN-only /dkv/* routes. DkvModule self-registers in the module registry. Complete dkvFleet i18n namespace and settings.smtp/categorySmtp/categoryGeneral keys added — Wave 3 frontend plans can run without touching message files.
## Tasks Completed
| Task | Name | Commit | Key Files |
|------|------|--------|-----------|
| 1 | DkvService — pipeline orchestration + vehicle/config/history | c40a023 | dkv.service.ts (305 LOC) |
| 2 | DkvSchedulerService + DkvController | c22d367 | dkv-scheduler.service.ts, dkv.controller.ts |
| 3 | DkvModule + seed + AppModule + i18n | 2a3d1c1 | dkv.module.ts, dkv.seed.ts, app.module.ts, de.json, en.json |
## Architecture Notes
### Pipeline Orchestration (DkvService)
```
processInbox(tenantId)
└── single-flight guard (this.processing flag)
└── load raw config + decrypt inbox creds (T-05-13: never log)
└── select provider by config.protocol (imap | exchange)
└── provider.fetchPdfAttachments(inboxConfig)
└── for each email.attachments:
└── 3-retry parsePdf (D-10: Fehler row on final fail)
└── batch load DkvVehicleMaster for tenant
└── build ExportRow[] (resolveFahrzeug + driver lookup)
└── DkvExportService.buildExcelBuffer + writeAndPrune
└── 3-retry DkvMailService.sendExportEmail (D-16)
└── exponential backoff: 2s, 4s
└── on fail: Versand fehlgeschlagen (file stays available)
└── prisma.dkvInvoiceHistory.create (D-20)
```
### Scheduler Multi-Tenant Decision (v1)
For v1, `DkvSchedulerService.onModuleInit()` calls `dkvService.loadConfig()` without a tenantId — which uses `findFirst()` to load the first active DkvModuleConfig row. One cron job is registered for that tenant's interval.
**Rationale:** Single-tenant deployments are the v1 target. Multi-tenant scheduling (one `SchedulerRegistry` job per active tenant, keyed as `dkv-inbox-poll-{tenantId}`) is deferred to a future plan.
**Impact:** Administrators with multiple tenants must set the schedule per-tenant; only the first active config is polled automatically.
### Circular Dependency Avoidance
`DkvSchedulerService` injects `DkvService` (to call `processInbox`). If `DkvService` also injected `DkvSchedulerService` (to re-apply the interval after `saveConfig`), this would create a circular dependency.
**Decision:** `DkvController` injects both services independently. After `PUT /dkv/config`, the controller calls `dkvScheduler.setInterval(...)` or `dkvScheduler.stopJob()`. Neither service injects the other.
### cron Package Resolution (pnpm strict isolation)
`CronJob` is from the `cron@4.4.0` package, which is a transitive dependency of `@nestjs/schedule@6.1.3`. Under pnpm strict isolation, transitive packages are not directly importable — `import { CronJob } from 'cron'` fails TypeScript's module resolution.
**Fix:** `CronJob` is resolved at runtime via `require('cron').CronJob` cast to a minimal interface `{ start(): void }`. The `schedulerRegistry.addCronJob()` call uses `as any` cast. At runtime, the object IS a full CronJob — SchedulerRegistry only calls `.stop()` on it. No new package install was needed (cron is already on disk).
### Invoice Number Extraction
DKV invoice numbers follow the pattern `\d{2}-\d{9}-\d{3}` (e.g., `26-651566449-001`). DkvService extracts this from `email.subject` via regex. Fallback when not found: `email-{uid}` using the IMAP UID or EWS item ID. This fallback still produces a unique, meaningful history record and filename.
## Verification Results
- `pnpm --filter @tessera/api type-check` exits 0: PASS (3x verified during execution)
- `grep -q "processing"` dkv.service.ts: PASS (single-flight guard present)
- `grep -q "encryptedInboxCreds"` dkv.service.ts: PASS (CONFIG_SAFE_SELECT + encryption handling)
- `grep -Eq "parsePdf|buildExcelBuffer|sendExportEmail"` dkv.service.ts: PASS
- `grep -q "schedulerRegistry.addCronJob"` dkv-scheduler.service.ts: PASS
- `grep -q "@Controller('dkv')"` dkv.controller.ts: PASS
- `grep -q "FileInterceptor"` dkv.controller.ts: PASS
- 12 handlers with `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`: PASS
- `grep -q "DkvModule"` app.module.ts: PASS
- `grep -q "seedDkvModule"` dkv.module.ts: PASS
- `grep -q "OnModuleInit"` dkv.module.ts: PASS
- i18n node validation (dkvFleet + settings.categorySmtp + settings.smtp): PASS
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] cron package not directly importable under pnpm strict isolation**
- **Found during:** Task 2 type-check
- **Issue:** `import { CronJob } from 'cron'` failed with TS2307 because `cron@4.4.0` is a transitive dependency of `@nestjs/schedule`, not declared in `apps/api/package.json`. pnpm strict isolation prevents phantom dependency imports.
- **Fix:** Replaced `import { CronJob } from 'cron'` with `require('cron').CronJob` cast to a minimal `{ start(): void }` interface. The `schedulerRegistry.addCronJob()` call uses `as any` to satisfy the `CronJob<null,null>` type expected by SchedulerRegistry. Functionally identical at runtime.
- **Files modified:** apps/api/src/dkv/dkv-scheduler.service.ts
- **Commit:** c22d367 (included in task commit)
### Architectural Decision
**DkvScheduler → saveConfig coordination:** The plan's action text said "after save calls the scheduler to (re)apply the interval". This could have been implemented as DkvService injecting DkvSchedulerService, but that creates a circular dependency (scheduler injects DkvService for processInbox). Instead, DkvController coordinates both services after PUT /dkv/config. This is a cleaner design with no NestJS `forwardRef()` workaround needed.
## Known Stubs
None. All pipeline orchestration, vehicle CRUD, CSV import, history, config and export download logic is fully implemented. The frontend Wave 3 plans (05, 06) can call all /dkv/* endpoints.
## Threat Flags
None. All STRIDE threats in this plan's threat register were mitigated:
- T-07-12: CONFIG_SAFE_SELECT excludes encryptedInboxCreds in all loadConfig/saveConfig responses
- T-05-13: Decrypted inbox credentials used only within method scope; never logged (generic error messages)
- T-07-09: getExportFile validates filename against `/^DKV_[\w\-]+\.xlsx$/` before fs.readFileSync; rejects path separators and `..`
- V4: All 12 DkvController handlers carry @Roles(Role.ADMIN, Role.SUPER_ADMIN)
## Self-Check: PASSED
Files verified present:
- apps/api/src/dkv/dkv.service.ts ✓
- apps/api/src/dkv/dkv-scheduler.service.ts ✓
- apps/api/src/dkv/dkv.controller.ts ✓
- apps/api/src/dkv/dkv.seed.ts ✓
- apps/api/src/dkv/dkv.module.ts ✓
- apps/api/src/app.module.ts (modified) ✓
- apps/web/src/messages/de.json (modified) ✓
- apps/web/src/messages/en.json (modified) ✓
Commits verified in git log:
- c40a023 ✓ (feat(07-04): DkvService — pipeline orchestration + vehicle/config/history logic)
- c22d367 ✓ (feat(07-04): DkvSchedulerService + DkvController — dynamic cron + REST surface)
- 2a3d1c1 ✓ (feat(07-04): DkvModule + registry seed + AppModule registration + i18n keys)