Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-01-PLAN.md
T
schalli de06794e67
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
docs(07): create phase 7 execution plans for DKV fleet module
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>
2026-06-26 18:56:59 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
07-dkv-fleet-module 01 execute 0
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
false
DKV-02
truths artifacts key_links
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)
path provides contains
apps/api/prisma/schema.prisma DkvModuleConfig, DkvVehicleMaster, DkvInvoiceHistory, SmtpConfig models model DkvModuleConfig
path provides min_lines
apps/api/src/dkv/dkv-parser.service.ts DkvParserService.parsePdf(buffer) -> DkvVehicleBlock[] 40
path provides
apps/api/src/dkv/dkv-parser.validate.ts Wave 0 validation script asserting >=1 vehicle block from invoice.pdf
path provides
apps/api/src/dkv/dkv.types.ts DkvVehicleBlock, DkvTransaction, InboxConfig, ExportRow shared types
from to via pattern
apps/api/src/dkv/dkv-parser.service.ts pdf-parse PDFParse class new PDFParse({ data: buffer }).getText() new PDFParse
from to via pattern
apps/api/src/app.module.ts @nestjs/schedule ScheduleModule ScheduleModule.forRoot() in imports array 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.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.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<DkvVehicleBlock[]>
  • 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.

<threat_model>

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
</threat_model>
- `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

<success_criteria>

  • 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 </success_criteria>
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.