de06794e67
6 plans covering full pipeline: PDF parsing foundation (Wave 0), inbox providers + export/SMTP services (Wave 1), pipeline orchestration + frontend pages + settings UI (Wave 2). Includes D-06 MailModule DB-config migration and Nyquist validation strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
187 lines
15 KiB
Markdown
187 lines
15 KiB
Markdown
---
|
|
phase: 07-dkv-fleet-module
|
|
plan: 04
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["07-01", "07-02", "07-03"]
|
|
files_modified:
|
|
- 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
|
|
autonomous: true
|
|
requirements: [DKV-01, DKV-03, DKV-04, DKV-05]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Triggering an inbox check runs the full pipeline: poll -> parse -> map drivers -> export xlsx -> send SMTP -> record history"
|
|
- "The poll interval is driven by a dynamic cron job sourced from DB config"
|
|
- "Concurrent processing is prevented by a single-flight guard"
|
|
- "All /dkv/* REST routes exist and are ADMIN-only"
|
|
- "Vehicle master CRUD and CSV import work end-to-end"
|
|
- "Export files are downloadable; history is paginated"
|
|
- "DKV module is registered in the module registry"
|
|
- "All dkvFleet and settings i18n keys exist in de.json and en.json"
|
|
artifacts:
|
|
- path: "apps/api/src/dkv/dkv.service.ts"
|
|
provides: "processInbox orchestration + vehicle CRUD + CSV import + config + history + driver mapping"
|
|
min_lines: 80
|
|
- path: "apps/api/src/dkv/dkv-scheduler.service.ts"
|
|
provides: "SchedulerRegistry dynamic cron lifecycle"
|
|
contains: "addCronJob"
|
|
- path: "apps/api/src/dkv/dkv.controller.ts"
|
|
provides: "all /dkv/* routes, ADMIN-only"
|
|
contains: "@Controller('dkv')"
|
|
- path: "apps/api/src/dkv/dkv.module.ts"
|
|
provides: "DkvModule wiring + registry self-seed"
|
|
contains: "OnModuleInit"
|
|
key_links:
|
|
- from: "apps/api/src/dkv/dkv.service.ts"
|
|
to: "DkvParserService / DkvExportService / DkvMailService / InboxProvider"
|
|
via: "pipeline orchestration"
|
|
pattern: "parsePdf|buildExcelBuffer|sendExportEmail"
|
|
- from: "apps/api/src/dkv/dkv-scheduler.service.ts"
|
|
to: "SchedulerRegistry"
|
|
via: "addCronJob with */N cron expression"
|
|
pattern: "schedulerRegistry.addCronJob"
|
|
- from: "apps/api/src/dkv/dkv.module.ts"
|
|
to: "ModuleRegistryService"
|
|
via: "seedDkvModule on init"
|
|
pattern: "seedDkvModule"
|
|
---
|
|
|
|
<objective>
|
|
Wire the full DKV processing pipeline together and expose it over REST. DkvService orchestrates poll → parse → driver-map → export → send → history with a single-flight guard and per-D-10/D-16 retry logic. DkvSchedulerService drives the configurable poll interval via SchedulerRegistry. DkvController exposes all /dkv/* routes (config, vehicles CRUD, CSV import, check-now, history, export download), ADMIN-only. DkvModule self-registers in the module registry. Finally, add all phase i18n keys so the Wave 3 frontend plans can run in parallel without touching the message files.
|
|
|
|
Purpose: This is the integration plane that turns the Plan 02/03 primitives into the end-to-end DKV-01/03/04/05 capability and exposes it to the frontend.
|
|
Output: DkvService, DkvSchedulerService, DkvController, DkvModule (+seed), AppModule registration, complete i18n keys.
|
|
</objective>
|
|
|
|
## Phase Goal
|
|
|
|
**Als** Administrator **möchte ich** DKV-Tankkarten-Rechnungen automatisch aus einem E-Mail-Postfach verarbeiten lassen, **damit** Flotten-Tankdaten ohne manuelle Eingabe als Excel-Datei exportiert und per SMTP zugestellt werden.
|
|
|
|
After this plan, the entire backend pipeline runs end-to-end via a single "check inbox" trigger.
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-CONTEXT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-RESEARCH.md
|
|
@.planning/phases/07-dkv-fleet-module/07-PATTERNS.md
|
|
@.planning/phases/07-dkv-fleet-module/07-UI-SPEC.md
|
|
@apps/api/src/dkv/dkv.types.ts
|
|
@apps/api/src/ldap/ldap.controller.ts
|
|
@apps/api/src/ldap/ldap-sync.scheduler.ts
|
|
@apps/api/src/domaincheck/domaincheck.module.ts
|
|
@apps/api/src/domaincheck/domaincheck.seed.ts
|
|
@apps/api/src/calendar/calendar.service.ts
|
|
</context>
|
|
|
|
## Artifacts this phase produces (Plan 04 portion)
|
|
|
|
New symbols (exclude from drift verification):
|
|
- Classes `DkvService`, `DkvSchedulerService`, `DkvController`, `DkvModule`
|
|
- Function `seedDkvModule` (registry seed)
|
|
- DkvService methods: `processInbox`, `checkNow`, `loadConfig`, `saveConfig`, `getHistory`, `listVehicles`, `createVehicle`, `updateVehicle`, `deleteVehicle`, `importVehiclesCsv`, `getExportFile`
|
|
- `DkvModule` registered in `apps/api/src/app.module.ts`
|
|
- i18n namespaces `dkvFleet` (new) and `settings` keys (categoryGeneral, categorySmtp, smtp.*)
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: DkvService pipeline orchestration + vehicle/config/history logic</name>
|
|
<files>apps/api/src/dkv/dkv.service.ts</files>
|
|
<read_first>
|
|
- apps/api/src/calendar/calendar.service.ts — orchestration service shell, CONFIG SAFE_SELECT pattern (exclude encryptedInboxCreds), crypto encrypt/decrypt usage
|
|
- apps/api/src/dkv/dkv.types.ts — DkvVehicleBlock, ExportRow, InboxConfig
|
|
- apps/api/src/dkv/providers/inbox-provider.interface.ts — InboxProvider contract (Plan 02)
|
|
- apps/api/src/dkv/dkv-parser.service.ts (Plan 01), dkv-export.service.ts + dkv-mail.service.ts (Plan 03) — methods to call
|
|
- 07-RESEARCH.md "Pitfall 7" (single-flight guard), "CSV Vehicle Import Pattern", D-09/D-10/D-13/D-16 decisions
|
|
- 07-CONTEXT.md D-12 filename, D-13 columns, D-19 vehicleFormatString
|
|
</read_first>
|
|
<action>
|
|
Implement `@Injectable() DkvService` injecting PrismaService, CalendarCryptoService, DkvParserService, DkvExportService, DkvMailService, ImapProvider, ExchangeInboxProvider.
|
|
Config: `loadConfig(tenantId?)` returns DkvModuleConfig via a CONFIG_SAFE_SELECT that excludes `encryptedInboxCreds`; `saveConfig(tenantId, dto)` encrypts `{username,password}` to `encryptedInboxCreds` via `crypto.encrypt(JSON.stringify(...))` (only when provided; preserve existing on empty), upserts on tenantId @unique, and after save calls the scheduler to (re)apply the interval.
|
|
Pipeline `processInbox(tenantId)`: single-flight guard (`private processing` flag per Pitfall 7 — return early if busy). Select provider by `config.protocol` ('imap'→ImapProvider, 'exchange'→ExchangeInboxProvider). Decrypt inbox creds, build InboxConfig, fetch PDF attachments. For each invoice PDF: parse via DkvParserService (D-10: up to 3 parse retries, then record a `Fehler` history row with errorMessage and continue); resolve each vehicle's driver by looking up DkvVehicleMaster by (tenantId, kennzeichen) — unknown plates still export with empty Fahrer; build ExportRow[] (Lieferdatum string TT.MM.JJJJ, Fahrzeug via export.resolveFahrzeug + config.vehicleFormatString, Fahrer, Ort, Kilometerstand number); generate xlsx buffer + writeAndPrune to get filename; send via DkvMailService with D-16 retry (3 attempts, exponential backoff) — on final failure record status `Versand fehlgeschlagen` (file stays available); on success record `Verarbeitet`. Each history row records datumZeit, rechnungsnummer, anzahlFahrzeuge, anzahlTransaktionen, status, exportFilename (D-20).
|
|
`checkNow(tenantId)`: invoke processInbox and return a summary; update a lastCheckedAt notion (store on config or return now()).
|
|
Vehicle CRUD: `listVehicles`, `createVehicle`, `updateVehicle`, `deleteVehicle` scoped by tenantId (unique on tenantId+kennzeichen). `importVehiclesCsv(tenantId, csvText, mode)`: semicolon-delimited parse (header Kennzeichen;Marke;Modell;Fahrer per Research); mode 'merge' upserts, mode 'replace' deletes all tenant vehicles then inserts (returns count). `getHistory(tenantId, page, limit)` paginated, ordered by datumZeit desc. `getExportFile(tenantId, filename)`: validate filename matches the `DKV_*.xlsx` server-format (reject anything with path separators — traversal guard) before reading from user-files/.
|
|
Never log decrypted credentials (T-05-13).
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "processing" apps/api/src/dkv/dkv.service.ts && grep -q "encryptedInboxCreds" apps/api/src/dkv/dkv.service.ts && grep -Eq "parsePdf|buildExcelBuffer|sendExportEmail" apps/api/src/dkv/dkv.service.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `processInbox` has a single-flight guard (early return when already processing)
|
|
- Pipeline calls parser, export builder, and mail sender in sequence
|
|
- Parse failures record a `Fehler` history row with errorMessage (D-10); SMTP final failure records `Versand fehlgeschlagen` (D-16)
|
|
- CONFIG_SAFE_SELECT excludes `encryptedInboxCreds`
|
|
- `getExportFile` rejects filenames containing path separators
|
|
- CSV import supports merge and replace modes
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>The full poll→parse→map→export→send→history pipeline plus vehicle/config/history logic is implemented with safety guards.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: DkvSchedulerService + DkvController</name>
|
|
<files>apps/api/src/dkv/dkv-scheduler.service.ts, apps/api/src/dkv/dkv.controller.ts</files>
|
|
<read_first>
|
|
- apps/api/src/ldap/ldap-sync.scheduler.ts — scheduler service shape (this one DIFFERS: dynamic SchedulerRegistry not static @Cron)
|
|
- 07-RESEARCH.md "Pattern 7: Dynamic Cron Job (SchedulerRegistry)" + "Pitfall 4" (ScheduleModule.forRoot already added in Plan 01)
|
|
- apps/api/src/ldap/ldap.controller.ts — controller shell, @Roles, tenant extraction, FileInterceptor usage pattern
|
|
- 07-PATTERNS.md "dkv.controller.ts" — full route list, FileInterceptor for CSV import
|
|
- apps/api/src/dkv/dto/dkv-config.dto.ts + dkv-vehicle.dto.ts + dkv-history.dto.ts (Plan 02)
|
|
</read_first>
|
|
<action>
|
|
DkvSchedulerService: `@Injectable() implements OnModuleInit`, inject SchedulerRegistry + DkvService. Constant `JOB_NAME='dkv-inbox-poll'`. `onModuleInit` loads config and, if isActive, calls `setInterval(pollIntervalMin)`. `setInterval(min)`: remove existing job (getCronJob/stop/deleteCronJob wrapped in try/catch), create `new CronJob('*/${min} * * * *', () => this.dkvService.processInbox(tenantId).catch(log))`, addCronJob + start. Expose a method DkvService can call after config save to re-apply the interval. (Multi-tenant note: if only one tenant config exists today, key the job by tenant or process all active tenant configs — document the choice in the SUMMARY.)
|
|
DkvController: `@Controller('dkv')`, EVERY handler `@Roles(Role.ADMIN, Role.SUPER_ADMIN)`, tenant from `req.tenantId` (BadRequestException when absent). Routes:
|
|
- `GET /dkv/config`, `PUT /dkv/config` (DkvConfigDto)
|
|
- `POST /dkv/check-now` → dkvService.checkNow
|
|
- `GET /dkv/history` (DkvHistoryQueryDto page/limit)
|
|
- `GET /dkv/exports/:filename` → stream the file (Content-Disposition attachment); rely on service traversal guard
|
|
- `GET /dkv/vehicles`, `POST /dkv/vehicles` (CreateVehicleDto), `PUT /dkv/vehicles/:id` (UpdateVehicleDto), `DELETE /dkv/vehicles/:id`
|
|
- `POST /dkv/vehicles/import` with `@UseInterceptors(FileInterceptor('file'))` + a `mode` body field ('merge'|'replace'); read uploaded buffer to utf8 and pass to dkvService.importVehiclesCsv
|
|
- `POST /dkv/test-connection` (DkvConfigDto) → provider.testConnection for the inbox config form
|
|
Follow the ldap.controller error-handling convention.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "schedulerRegistry.addCronJob" apps/api/src/dkv/dkv-scheduler.service.ts && grep -q "@Controller('dkv')" apps/api/src/dkv/dkv.controller.ts && grep -q "FileInterceptor" apps/api/src/dkv/dkv.controller.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Scheduler uses `schedulerRegistry.addCronJob` (not a static `@Cron` decorator)
|
|
- All DkvController handlers carry `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` (V4)
|
|
- Routes exist for config GET/PUT, check-now, history, exports/:filename, vehicles CRUD, vehicles/import, test-connection
|
|
- CSV import route uses `FileInterceptor`
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>Dynamic cron scheduling and the complete ADMIN-only REST surface are in place.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: DkvModule + registry seed + AppModule registration + i18n keys</name>
|
|
<files>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</files>
|
|
<read_first>
|
|
- apps/api/src/domaincheck/domaincheck.module.ts — module + OnModuleInit + ModuleRegistryModule import + seed call
|
|
- apps/api/src/domaincheck/domaincheck.seed.ts — seedModule manifest shape (slug, name, version, category, description {de,en}, isSystem)
|
|
- 07-PATTERNS.md "dkv.module.ts" (provider list incl. CalendarCryptoService) + "AppModule Modification"
|
|
- 07-UI-SPEC.md "i18n Translation Keys" — full dkvFleet namespace + settings namespace additions (DE values given; provide EN equivalents)
|
|
- apps/web/src/messages/de.json + en.json — existing structure to extend (do not remove existing keys)
|
|
</read_first>
|
|
<action>
|
|
Create dkv.seed.ts exporting `seedDkvModule(moduleRegistryService)` calling `seedModule({ slug: 'dkv-fleet', name: 'DKV Flotte', version: '1.0.0', category: 'fleet', description: { de: 'DKV-Tankkartenrechnungen automatisch verarbeiten', en: 'Automatically process DKV fuel card invoices' }, isSystem: true })`.
|
|
Create dkv.module.ts: `@Module` importing `ModuleRegistryModule` (for the registry seed) and providing DkvService, DkvSchedulerService, DkvParserService, DkvExportService, DkvMailService, ImapProvider, ExchangeInboxProvider, CalendarCryptoService; controllers [DkvController]; exports [DkvService]. Implement `OnModuleInit` to call `seedDkvModule(this.moduleRegistryService)` in a try/catch with logger (mirror DomaincheckModule).
|
|
Register `DkvModule` in apps/api/src/app.module.ts imports array (ScheduleModule.forRoot and SettingsModule were added by Plans 01 and 03 respectively — only add DkvModule here).
|
|
Add the complete `dkvFleet` namespace to both apps/web/src/messages/de.json and en.json using the exact German strings from 07-UI-SPEC.md "i18n Translation Keys", and provide accurate English translations for en.json. Extend the existing `settings` namespace with `categoryGeneral`, `categorySmtp`, and the `smtp` sub-object (both files). Preserve all existing keys; valid JSON in both files.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "DkvModule" apps/api/src/app.module.ts && grep -q "seedDkvModule" apps/api/src/dkv/dkv.module.ts && node -e "const d=require('./apps/web/src/messages/de.json');const e=require('./apps/web/src/messages/en.json');if(!d.dkvFleet||!e.dkvFleet||!d.settings.categorySmtp||!e.settings.categorySmtp)process.exit(1);console.log('i18n ok')" |