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).
10 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 | 04 | dkv-integration-plane |
|
|
|
|
|
|
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-checkexits 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: PASSgrep -q "schedulerRegistry.addCronJob"dkv-scheduler.service.ts: PASSgrep -q "@Controller('dkv')"dkv.controller.ts: PASSgrep -q "FileInterceptor"dkv.controller.ts: PASS- 12 handlers with
@Roles(Role.ADMIN, Role.SUPER_ADMIN): PASS grep -q "DkvModule"app.module.ts: PASSgrep -q "seedDkvModule"dkv.module.ts: PASSgrep -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 becausecron@4.4.0is a transitive dependency of@nestjs/schedule, not declared inapps/api/package.json. pnpm strict isolation prevents phantom dependency imports. - Fix: Replaced
import { CronJob } from 'cron'withrequire('cron').CronJobcast to a minimal{ start(): void }interface. TheschedulerRegistry.addCronJob()call usesas anyto satisfy theCronJob<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: