--- phase: 07-dkv-fleet-module plan: 01 type: execute wave: 0 depends_on: [] files_modified: - apps/api/package.json - apps/api/prisma/schema.prisma - apps/api/src/app.module.ts - apps/api/src/calendar/calendar.module.ts - apps/api/src/dkv/dkv.types.ts - apps/api/src/dkv/dkv-parser.validate.ts - apps/api/src/dkv/dkv-parser.service.ts autonomous: false requirements: [DKV-02] user_setup: [] must_haves: truths: - "The four DKV tables exist in the live PostgreSQL database (DkvModuleConfig, DkvVehicleMaster, DkvInvoiceHistory, SmtpConfig)" - "ScheduleModule is registered in AppModule so dynamic cron jobs can be created" - "The DKV parser extracts at least one vehicle block from user-files/invoice.pdf" - "CalendarCryptoService is importable by other modules (exported from CalendarModule)" artifacts: - path: "apps/api/prisma/schema.prisma" provides: "DkvModuleConfig, DkvVehicleMaster, DkvInvoiceHistory, SmtpConfig models" contains: "model DkvModuleConfig" - path: "apps/api/src/dkv/dkv-parser.service.ts" provides: "DkvParserService.parsePdf(buffer) -> DkvVehicleBlock[]" min_lines: 40 - path: "apps/api/src/dkv/dkv-parser.validate.ts" provides: "Wave 0 validation script asserting >=1 vehicle block from invoice.pdf" - path: "apps/api/src/dkv/dkv.types.ts" provides: "DkvVehicleBlock, DkvTransaction, InboxConfig, ExportRow shared types" key_links: - from: "apps/api/src/dkv/dkv-parser.service.ts" to: "pdf-parse PDFParse class" via: "new PDFParse({ data: buffer }).getText()" pattern: "new PDFParse" - from: "apps/api/src/app.module.ts" to: "@nestjs/schedule ScheduleModule" via: "ScheduleModule.forRoot() in imports array" pattern: "ScheduleModule.forRoot" --- Establish the backend foundation for the DKV Fleet Module: install the three new npm packages, add the four Prisma data models, push the schema to the live database, register ScheduleModule, export the shared crypto service, and validate the highest-risk component — the DKV PDF parser — against the real reference invoice before any production code consumes it. Purpose: The PDF parsing regex is the single LOW-confidence element of this phase (Research A2). Validating it in Wave 0 against user-files/invoice.pdf de-risks every downstream plan. The schema + ScheduleModule + crypto export are hard prerequisites for all backend plans. Output: Installed deps, four live DB tables, registered ScheduleModule, exported CalendarCryptoService, a validated DkvParserService and a runnable validation script. ## 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. This plan delivers the foundation that proves the riskiest leg of that pipeline (PDF → structured data) works against the real DKV invoice. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.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 ## Artifacts this phase produces (Plan 01 portion) New symbols introduced here (exclude from drift verification): - Prisma models: `DkvModuleConfig`, `DkvVehicleMaster`, `DkvInvoiceHistory`, `SmtpConfig` - File `apps/api/src/dkv/dkv.types.ts`: interfaces `DkvTransaction`, `DkvVehicleBlock`, `InboxConfig`, `InboxEmail`, `InboxAttachment`, `ExportRow` - Class `DkvParserService` with method `parsePdf(buffer: Buffer): Promise` - Validation script `apps/api/src/dkv/dkv-parser.validate.ts` - New deps in apps/api/package.json: `imapflow`, `pdf-parse`, `xlsx` - `CalendarCryptoService` added to `CalendarModule` `exports` Task 1: Verify imapflow package legitimacy before install Nothing yet — this gate runs BEFORE any package install. The package legitimacy audit in 07-RESEARCH.md flags `imapflow` as SUS (a too-new version was published the same day as research). The audit disposition is "Approved — false positive" because the package has existed since 2019 with ~1.1M weekly downloads from github.com/postalsys/imapflow. Confirm before install: 1. Open https://www.npmjs.com/package/imapflow — confirm repository is `postalsys/imapflow`, weekly downloads are in the ~1M range, and the package is not a recent typosquat. 2. Confirm `pdf-parse` (https://www.npmjs.com/package/pdf-parse, repo mehmet-kozan/pdf-parse) and `xlsx` (https://www.npmjs.com/package/xlsx, repo SheetJS/sheetjs) match the audit table. Type "approved" to proceed with install, or describe a concern to halt. Task 2: Install deps, add Prisma models, wire ScheduleModule + crypto export apps/api/package.json, apps/api/prisma/schema.prisma, apps/api/src/app.module.ts, apps/api/src/calendar/calendar.module.ts, apps/api/src/dkv/dkv.types.ts - apps/api/prisma/schema.prisma — current schema end (CalendarSource is the last model; append after it, do not edit existing models) - apps/api/src/app.module.ts — current imports array (ScheduleModule is NOT present; CalendarModule, DashboardModule etc. are) - apps/api/src/calendar/calendar.module.ts — current `exports: [CalendarService]` line (must become `exports: [CalendarService, CalendarCryptoService]`) - 07-RESEARCH.md "Prisma Schema (new tables)" — exact field definitions for all four models - 07-RESEARCH.md "Pattern 8: ScheduleModule Registration" Install the three new packages into the api workspace: run `pnpm --filter @tessera/api add imapflow pdf-parse xlsx`. Do NOT add @types/* — all three ship their own types. Append exactly the four Prisma models from 07-RESEARCH.md "Prisma Schema (new tables)" to apps/api/prisma/schema.prisma: `DkvModuleConfig` (tenantId @unique, protocol default "imap", encryption default "ssl-tls", folder default "INBOX", pollIntervalMin default 60, isActive default false, vehicleFormatString default "{Marke}/{Modell}/{Kennzeichen}", encryptedInboxCreds String?), `DkvVehicleMaster` (@@unique([tenantId, kennzeichen])), `DkvInvoiceHistory` (status string, errorMessage String?, exportFilename String?, indices on tenantId and datumZeit), `SmtpConfig` (tenantId @unique, port default 587, encryption default "starttls", encryptedPassword String?, fromAddress required). Use the exact field names and defaults — downstream code depends on these identifiers. In apps/api/src/app.module.ts add `import { ScheduleModule } from '@nestjs/schedule';` and add `ScheduleModule.forRoot()` to the imports array (DkvModule and SettingsModule are registered in later plans — do NOT add them here, they do not exist yet). In apps/api/src/calendar/calendar.module.ts change the `exports` array to also export `CalendarCryptoService` so DkvModule and SettingsModule can inject it (per PATTERNS.md note). Create apps/api/src/dkv/dkv.types.ts containing the shared interfaces `DkvTransaction` (lieferdatum string, ort string, kilometerstand number, produkt string, menge number, einheit string, netto number, brutto number), `DkvVehicleBlock` (kennzeichen string, cardNumber string, transactions DkvTransaction[]), `InboxConfig` (protocol, host, port, username?, password?, encryption, folder, senderFilter?), `InboxAttachment` (filename, contentType, buffer Buffer), `InboxEmail` (uid, messageId, subject, from, date, attachments InboxAttachment[]), and `ExportRow` (lieferdatum, fahrzeug, fahrer, ort string, kilometerstand number) — field shapes taken from 07-RESEARCH.md interfaces. pnpm --filter @tessera/api exec node -e "require('imapflow');require('pdf-parse');require('xlsx');console.log('deps ok')" && grep -q "model DkvModuleConfig" apps/api/prisma/schema.prisma && grep -q "model SmtpConfig" apps/api/prisma/schema.prisma && grep -q "ScheduleModule.forRoot" apps/api/src/app.module.ts && grep -q "CalendarCryptoService" apps/api/src/calendar/calendar.module.ts - `apps/api/package.json` dependencies include `imapflow`, `pdf-parse`, and `xlsx` - `grep -c "^model " apps/api/prisma/schema.prisma` increased by exactly 4 versus before the change - `apps/api/prisma/schema.prisma` contains `model DkvModuleConfig`, `model DkvVehicleMaster`, `model DkvInvoiceHistory`, `model SmtpConfig` - `apps/api/src/app.module.ts` contains `ScheduleModule.forRoot()` inside the imports array - `apps/api/src/calendar/calendar.module.ts` exports array includes `CalendarCryptoService` - `apps/api/src/dkv/dkv.types.ts` exports interfaces `DkvVehicleBlock`, `DkvTransaction`, `InboxConfig`, `InboxEmail`, `InboxAttachment`, `ExportRow` Deps installed, four models present, ScheduleModule registered, crypto exported, shared types file created. Task 3 [BLOCKING]: Push schema to live database apps/api/prisma/schema.prisma - .planning/phases/05-dashboard-calendar/05-03-PLAN.md (db push task) — established push pattern for this project (DB port not exposed to host; push from container or via container IP) - .planning/phases/05-dashboard-calendar/05-01-SUMMARY.md — note "Prisma db push via docker exec" workaround This is the MANDATORY schema-push task required by the phase planning contract. Build and type checks pass WITHOUT the push (types come from the generated client, not the live DB), so this is a false-positive risk if skipped. Push the new tables to the running PostgreSQL container following the project's established method from Phase 05 (the DB port is not exposed to the host — push from inside the api container via `docker compose exec` or via the container IP, whichever Phase 05 SUMMARYs recorded as working). Command core: `prisma db push --skip-generate` then `prisma generate`. Only four NEW tables are added — no existing table changes. If the push reports it would cause data loss, STOP and flag for manual review; do NOT blindly pass `--accept-data-loss` (the non-TTY workaround is only acceptable if the sole reported change is the four additive tables). cd apps/api && npx prisma db push --skip-generate 2>&1 | grep -Eq "already in sync|now in sync|in sync with" && npx prisma generate >/dev/null 2>&1 && echo "schema pushed" - `prisma db push` exits 0 - A second `prisma db push` run reports the database is already in sync - `prisma generate` regenerates the client without error - No data-loss warning was bypassed with `--accept-data-loss` The four DKV tables exist in the live database and the Prisma client is regenerated. Task 4: DKV PDF parser — validate against real invoice, then implement service apps/api/src/dkv/dkv-parser.validate.ts, apps/api/src/dkv/dkv-parser.service.ts - user-files/invoice.pdf — the real DKV E-Rechnung (April 2026, 4 pages, 27 vehicles) — parse target - 07-RESEARCH.md "Pattern 3: pdf-parse v2 Text Extraction" — class-based API `new PDFParse({ data: buffer })`, must call `destroy()` - 07-RESEARCH.md "Pattern 4: DKV PDF Regex" — vehicle block + transaction row regex (ASSUMED, must be validated/adjusted against real output) - 07-RESEARCH.md "Pitfall 2" (v1 vs v2 API break) and "Pitfall 5" (German number format: strip dots, replace comma with dot) - apps/api/src/dkv/dkv.types.ts — DkvVehicleBlock / DkvTransaction shapes to return - extractPdfText(buffer): returns concatenated text from all pages of invoice.pdf (non-empty string) - parseDkvText(text): returns >= 1 DkvVehicleBlock; for invoice.pdf it should approach 27 vehicle blocks (the reference invoice has 27 vehicles) - German number parsing: "19.234,56" parses to 19234.56 (not 19.234) - Each block has a non-empty `kennzeichen` and `transactions` array First write apps/api/src/dkv/dkv-parser.validate.ts: a standalone script that reads `user-files/invoice.pdf` via fs.readFileSync, runs the pdf-parse v2 extraction (`new PDFParse({ data: buffer })`, `getText()`, `destroy()`), then runs the vehicle-block regex from Research Pattern 4. It MUST print the parsed structure (vehicle count, first block's kennzeichen + transaction count) AND assert: if zero vehicle blocks are parsed, print the raw extracted text to stdout and `process.exit(1)`. This script is the empirical source of truth — adjust the regex in this script until it parses the real invoice, THEN copy the proven logic into the service. Then implement apps/api/src/dkv/dkv-parser.service.ts as an `@Injectable()` NestJS service (Logger via `new Logger(DkvParserService.name)`) with `async parsePdf(buffer: Buffer): Promise` that wraps the validated extract+parse logic. Apply German number parsing `parseFloat(raw.replace(/\./g, '').replace(',', '.'))` to kilometerstand and menge. Never log decrypted credentials (not applicable here, but follow T-05-13 generic-error convention for any catch). If extraction yields zero vehicles, throw an Error with a generic message (caller records it as parse failure per D-10). Do NOT use the v1 `pdfParse(buffer)` call — it does not exist in v2. pnpm --filter @tessera/api type-check && cd apps/api && (node --experimental-strip-types src/dkv/dkv-parser.validate.ts || node --no-warnings --experimental-strip-types src/dkv/dkv-parser.validate.ts) - `apps/api/src/dkv/dkv-parser.validate.ts` exits 0 and prints a vehicle count >= 1 when run against user-files/invoice.pdf - On zero parsed vehicles the script exits 1 (proves the assertion is wired) - `apps/api/src/dkv/dkv-parser.service.ts` exports class `DkvParserService` with method `parsePdf` - Source contains `new PDFParse(` and does NOT contain a bare `pdfParse(` v1 call - German-number handling present: `replace(/\\./g, '').replace(',', '.')` appears in the parse path - `pnpm --filter @tessera/api type-check` exits 0 The parser is empirically validated against the real DKV invoice and wrapped in an injectable service returning typed vehicle blocks. ## Trust Boundaries | Boundary | Description | |----------|-------------| | npm registry → build | Third-party packages enter the supply chain at install time | | PDF bytes → parser | Untrusted binary attachment content is parsed server-side | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-07-SC | Tampering | npm installs (imapflow/pdf-parse/xlsx) | mitigate | Package Legitimacy Audit (07-RESEARCH.md) + blocking-human checkpoint (Task 1) verifies imapflow SUS flag is a false positive against npmjs.com before install | | T-07-01 | Denial of Service | dkv-parser.service.ts | mitigate | Parser operates only on attachments fetched by the inbox provider, which enforces a max attachment size (Plan 02). Parser calls `destroy()` to free memory per pdf-parse guidance | | T-07-02 | Information Disclosure | dkv-parser.service.ts | mitigate | Generic error messages only on parse failure (T-05-13 convention); no PDF content echoed in production logs | - `pnpm --filter @tessera/api type-check` exits 0 - `grep -q "model DkvModuleConfig" apps/api/prisma/schema.prisma` succeeds for all four models - `prisma db push` reports in sync on second run - Validation script parses >= 1 vehicle block from user-files/invoice.pdf - Three packages installed; four tables live in PostgreSQL; ScheduleModule registered; CalendarCryptoService exported - DkvParserService validated against the real invoice and returns typed vehicle blocks - DKV-02 parsing risk (Research A2) retired before downstream plans build on it Create `.planning/phases/07-dkv-fleet-module/07-01-SUMMARY.md` when done. Record: actual vehicle count parsed from invoice.pdf, any regex adjustments made versus Research Pattern 4, and the exact db push method used.