docs(07-04): complete DKV integration plane plan
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:
+14
-9
@@ -3,9 +3,9 @@ gsd_state_version: 1.0
|
|||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: executing
|
||||||
stopped_at: Phase 07 Plan 03 complete
|
stopped_at: Phase 07 Plan 04 complete
|
||||||
last_updated: "2026-06-27T00:20:00.000Z"
|
last_updated: "2026-06-26T22:30:00.000Z"
|
||||||
last_activity: 2026-06-27 -- Phase 07 Plan 03 completed (DKV export + SMTP settings)
|
last_activity: 2026-06-26 -- Phase 07 Plan 04 completed (DKV integration plane + REST + i18n)
|
||||||
progress:
|
progress:
|
||||||
total_phases: 7
|
total_phases: 7
|
||||||
completed_phases: 5
|
completed_phases: 5
|
||||||
@@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-18)
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 07 (dkv-fleet-module) — EXECUTING
|
Phase: 07 (dkv-fleet-module) — EXECUTING
|
||||||
Plan: 4 of 6
|
Plan: 5 of 6
|
||||||
Status: Executing Phase 07 (Plan 03 complete)
|
Status: Executing Phase 07 (Plan 04 complete)
|
||||||
Last activity: 2026-06-27 -- Phase 07 Plan 03 completed (DKV export + SMTP settings)
|
Last activity: 2026-06-26 -- Phase 07 Plan 04 completed (DKV integration plane + REST + i18n)
|
||||||
|
|
||||||
Progress: [██████████] 100%
|
Progress: [██████████] 100%
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ Progress: [██████████] 100%
|
|||||||
| Phase 06-desktop-client-ci-cd P01 | 5min | 2 tasks | 13 files |
|
| Phase 06-desktop-client-ci-cd P01 | 5min | 2 tasks | 13 files |
|
||||||
| Phase 06 P03 | 3min | 3 tasks | 3 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 P03 | 5min | 4 tasks | 8 files |
|
||||||
|
| Phase 07-dkv-fleet-module P04 | 7min | 3 tasks | 8 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## 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]: 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]: 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-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
|
### Pending Todos
|
||||||
|
|
||||||
@@ -127,6 +132,6 @@ Items acknowledged and carried forward from previous milestone close:
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-06-27T00:20:00.000Z
|
Last session: 2026-06-26T22:30:00.000Z
|
||||||
Stopped at: Phase 07 Plan 03 complete (DKV export + SMTP settings)
|
Stopped at: Phase 07 Plan 04 complete (DKV integration plane)
|
||||||
Resume file: .planning/phases/07-dkv-fleet-module/07-04-PLAN.md
|
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)
|
||||||
Reference in New Issue
Block a user