# Phase 7: DKV Fleet Module - Pattern Map **Mapped:** 2026-06-26 **Files analyzed:** 21 new/modified files **Analogs found:** 19 / 21 --- ## File Classification | New/Modified File | Role | Data Flow | Closest Analog | Match Quality | |-------------------|------|-----------|----------------|---------------| | `apps/api/src/dkv/dkv.module.ts` | module | — | `apps/api/src/calendar/calendar.module.ts` | exact | | `apps/api/src/dkv/dkv.controller.ts` | controller | request-response | `apps/api/src/ldap/ldap.controller.ts` | exact | | `apps/api/src/dkv/dkv.service.ts` | service | orchestration | `apps/api/src/calendar/calendar.service.ts` | role-match | | `apps/api/src/dkv/dkv-scheduler.service.ts` | service | event-driven | `apps/api/src/ldap/ldap-sync.scheduler.ts` | exact | | `apps/api/src/dkv/dkv-parser.service.ts` | service | transform | `apps/api/src/calendar/calendar.service.ts` | partial | | `apps/api/src/dkv/dkv-export.service.ts` | service | file-I/O | — | no analog | | `apps/api/src/dkv/dkv-mail.service.ts` | service | request-response | `apps/api/src/mail/mail.service.ts` | role-match | | `apps/api/src/dkv/providers/inbox-provider.interface.ts` | interface | — | `apps/api/src/calendar/calendar.service.ts` (CalendarProvider) | exact | | `apps/api/src/dkv/providers/imap.provider.ts` | provider | file-I/O | `apps/api/src/calendar/providers/caldav.provider.ts` | role-match | | `apps/api/src/dkv/providers/exchange-inbox.provider.ts` | provider | request-response | `apps/api/src/calendar/providers/exchange.provider.ts` | exact | | `apps/api/src/dkv/dto/dkv-config.dto.ts` | dto | — | `apps/api/src/calendar/dto/create-calendar-source.dto.ts` | exact | | `apps/api/src/dkv/dto/dkv-vehicle.dto.ts` | dto | — | `apps/api/src/calendar/dto/create-calendar-source.dto.ts` | exact | | `apps/api/src/dkv/dto/dkv-history.dto.ts` | dto | — | `apps/api/src/calendar/dto/create-calendar-source.dto.ts` | role-match | | `apps/api/src/settings/settings.module.ts` | module | — | `apps/api/src/calendar/calendar.module.ts` | role-match | | `apps/api/src/settings/settings.controller.ts` | controller | request-response | `apps/api/src/ldap/ldap.controller.ts` | exact | | `apps/api/src/settings/settings.service.ts` | service | CRUD | `apps/api/src/calendar/calendar.service.ts` | role-match | | `apps/api/src/settings/dto/smtp-config.dto.ts` | dto | — | `apps/api/src/calendar/dto/create-calendar-source.dto.ts` | role-match | | `apps/api/src/app.module.ts` (modify) | module | — | self | — | | `apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx` | component | request-response | `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` | exact | | `apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx` | component | CRUD | `apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx` | role-match | | `apps/web/src/app/(portal)/settings/general/smtp/page.tsx` | component | CRUD | `apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx` | role-match | --- ## Pattern Assignments ### `apps/api/src/dkv/dkv.module.ts` (module) **Analog:** `apps/api/src/calendar/calendar.module.ts` **Module structure pattern** (lines 1–33): ```typescript import { Module } from '@nestjs/common'; import { DkvController } from './dkv.controller'; import { DkvService } from './dkv.service'; import { DkvSchedulerService } from './dkv-scheduler.service'; import { DkvParserService } from './dkv-parser.service'; import { DkvExportService } from './dkv-export.service'; import { DkvMailService } from './dkv-mail.service'; import { ImapProvider } from './providers/imap.provider'; import { ExchangeInboxProvider } from './providers/exchange-inbox.provider'; import { CalendarCryptoService } from '../calendar/crypto.service'; @Module({ controllers: [DkvController], providers: [ DkvService, DkvSchedulerService, DkvParserService, DkvExportService, DkvMailService, ImapProvider, ExchangeInboxProvider, CalendarCryptoService, // imported from CalendarModule — inject directly, not re-declared ], exports: [DkvService], }) export class DkvModule {} ``` **Note:** `CalendarCryptoService` must also be exported from `CalendarModule` (add `exports: [CalendarCryptoService]` there) so DkvModule can import it. --- ### `apps/api/src/dkv/dkv.controller.ts` (controller, request-response) **Analog:** `apps/api/src/ldap/ldap.controller.ts` **Imports pattern** (lines 1–19): ```typescript import { BadRequestException, Body, Controller, Delete, Get, NotFoundException, Param, Patch, Post, Req, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { Role } from '@prisma/client'; import { Roles } from '../auth/decorators/roles.decorator'; import { DkvService } from './dkv.service'; import { DkvConfigDto } from './dto/dkv-config.dto'; import { CreateVehicleDto, UpdateVehicleDto } from './dto/dkv-vehicle.dto'; ``` **Auth/Guard pattern** — ADMIN-only, same as `ldap.controller.ts` lines 38–40: ```typescript @Roles(Role.ADMIN, Role.SUPER_ADMIN) ``` Apply to every handler. Global `JwtAuthGuard` already enforces JWT; `@Roles` adds role check. **Tenant extraction pattern** — copy from `ldap.controller.ts` lines 40–47: ```typescript const tenantId = req.tenantId; if (!tenantId) { throw new BadRequestException('No tenant context'); } ``` **Core routes pattern:** ```typescript @Controller('dkv') export class DkvController { constructor(private readonly dkvService: DkvService) {} // GET /dkv/config — get module config (no decrypted passwords) // PUT /dkv/config — save module config // POST /dkv/check-now — manual inbox poll trigger // GET /dkv/history — processing history (paginated) // GET /dkv/exports/:filename — file download // GET /dkv/vehicles — list vehicle master // POST /dkv/vehicles — create vehicle // PUT /dkv/vehicles/:id — update vehicle // DELETE /dkv/vehicles/:id — delete vehicle // POST /dkv/vehicles/import — CSV bulk import } ``` **Error handling pattern** — follow `ldap.controller.ts`: ```typescript try { const result = await this.dkvService.someMethod(tenantId, dto); return result; } catch (error) { if (error instanceof NotFoundException) throw error; if (error instanceof BadRequestException) throw error; throw error; // let global exception filter handle it } ``` --- ### `apps/api/src/dkv/dkv.service.ts` (service, orchestration) **Analog:** `apps/api/src/calendar/calendar.service.ts` **Imports pattern** (lines 1–14 of calendar.service.ts): ```typescript import { Injectable, Logger, NotFoundException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CalendarCryptoService } from '../calendar/crypto.service'; // + DkvParserService, DkvExportService, DkvMailService, providers ``` **Safe DB select pattern** — never return encrypted credentials (calendar.service.ts lines 50–67): ```typescript const CONFIG_SAFE_SELECT = { id: true, tenantId: true, protocol: true, host: true, port: true, encryption: true, folder: true, senderFilter: true, pollIntervalMin: true, isActive: true, exportRecipient: true, vehicleFormatString: true, // encryptedInboxCreds: NEVER included createdAt: true, updatedAt: true, } as const; ``` **Credential encryption pattern** (from RESEARCH.md Code Examples): ```typescript // Encrypt at save time const encryptedInboxCreds = this.crypto.encrypt( JSON.stringify({ username: dto.username, password: dto.password }) ); // Decrypt at use time (never log result — T-05-13) const { username, password } = JSON.parse( this.crypto.decrypt(config.encryptedInboxCreds!) ); ``` **Error handling — generic messages** (exchange.provider.ts lines 42–48): ```typescript } catch (error) { this.logger.error( `DKV inbox poll failed for tenant ${tenantId}: ${(error as Error).message}`, ); // T-05-13: no credential details in error return []; } ``` --- ### `apps/api/src/dkv/dkv-scheduler.service.ts` (service, event-driven) **Analog:** `apps/api/src/ldap/ldap-sync.scheduler.ts` **Full structure pattern** (ldap-sync.scheduler.ts lines 1–89): ```typescript import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; // DkvSchedulerService DIFFERS: uses SchedulerRegistry + dynamic CronJob // instead of static @Cron — because interval is configurable from DB import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { SchedulerRegistry } from '@nestjs/schedule'; import { CronJob } from 'cron'; const JOB_NAME = 'dkv-inbox-poll'; @Injectable() export class DkvSchedulerService implements OnModuleInit { private readonly logger = new Logger(DkvSchedulerService.name); constructor( private readonly schedulerRegistry: SchedulerRegistry, private readonly dkvService: DkvService, ) {} onModuleInit() { this.dkvService.loadConfig().then(config => { if (config?.isActive) this.setInterval(config.pollIntervalMin); }).catch(err => this.logger.error('Failed to init DKV scheduler', err)); } setInterval(intervalMin: number): void { try { this.schedulerRegistry.getCronJob(JOB_NAME).stop(); this.schedulerRegistry.deleteCronJob(JOB_NAME); } catch { /* not yet registered */ } const job = new CronJob(`*/${intervalMin} * * * *`, () => { this.dkvService.processInbox().catch(err => this.logger.error('DKV inbox poll failed', err) ); }); this.schedulerRegistry.addCronJob(JOB_NAME, job); job.start(); } } ``` **Key difference from LdapSyncScheduler:** Static `@Cron()` decorator cannot change at runtime. Use `SchedulerRegistry.addCronJob()` pattern instead. --- ### `apps/api/src/dkv/dkv-parser.service.ts` (service, transform) **No direct analog** — unique PDF parsing logic. Use NestJS `@Injectable()` service wrapper. **Service shell pattern** (follows all other services): ```typescript import { Injectable, Logger } from '@nestjs/common'; @Injectable() export class DkvParserService { private readonly logger = new Logger(DkvParserService.name); async parsePdf(buffer: Buffer): Promise { // Wave 0: validate pdf-parse v2 API against user-files/invoice.pdf first // then implement production regex } } ``` **Critical: pdf-parse v2 API** — old `pdfParse(buffer)` function call does NOT exist in v2: ```typescript import { PDFParse } from 'pdf-parse'; async function extractPdfText(buffer: Buffer): Promise { const parser = new PDFParse({ data: buffer }); const result = await parser.getText(); await parser.destroy(); return result.text; } ``` **DKV vehicle block regex** (ASSUMED — validate against `user-files/invoice.pdf` in Wave 0): ```typescript const vehicleBlockPattern = /VEHICLE:\s+(\S+)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g; ``` **German number parsing** (mandatory for Kilometerstand/Menge): ```typescript parseFloat(raw.replace(/\./g, '').replace(',', '.')) ``` --- ### `apps/api/src/dkv/dkv-export.service.ts` (service, file-I/O) **No analog** — first file-I/O service in codebase. Use NestJS `@Injectable()` pattern. **SheetJS buffer pattern** (RESEARCH.md Pattern 5): ```typescript import * as XLSX from 'xlsx'; function buildExcelBuffer(rows: ExportRow[]): Buffer { const headers = ['Lieferdatum', 'Fahrzeug', 'Fahrer', 'Ort', 'Kilometerstand']; const data = rows.map(r => [r.lieferdatum, r.fahrzeug, r.fahrer, r.ort, r.kilometerstand]); const ws = XLSX.utils.aoa_to_sheet([headers, ...data]); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'DKV Export'); return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer; } ``` **Fahrzeug format string resolver** (RESEARCH.md Code Examples): ```typescript function resolveFahrzeug(vehicle: DkvVehicleMaster, formatString: string): string { return formatString .replace('{Marke}', vehicle.marke) .replace('{Modell}', vehicle.modell) .replace('{Kennzeichen}', vehicle.kennzeichen) .replace('{Fahrer}', vehicle.fahrer); } ``` **10-file prune guard** — add processing lock to prevent race: ```typescript private processing = false; async processInbox(): Promise { if (this.processing) return; // Pitfall 7: race condition guard this.processing = true; try { /* ... */ } finally { this.processing = false; } } ``` --- ### `apps/api/src/dkv/dkv-mail.service.ts` (service, request-response) **Analog:** `apps/api/src/mail/mail.service.ts` **Key difference:** Do NOT use `MailerService` from `@nestjs-modules/mailer`. Use `nodemailer.createTransport()` directly — transport created at send time from DB config (Pitfall 3). **Imports pattern** (mail.service.ts lines 1–4, adapted): ```typescript import { Injectable, Logger } from '@nestjs/common'; import * as nodemailer from 'nodemailer'; import { PrismaService } from '../prisma/prisma.service'; import { CalendarCryptoService } from '../calendar/crypto.service'; ``` **Error handling pattern** (mail.service.ts lines 69–78): ```typescript try { await transport.sendMail({ ... }); this.logger.log(`DKV export sent to ${recipient}`); } catch (error) { // Log but surface to caller for retry logic (unlike MailService which swallows) this.logger.error( `Failed to send DKV export to ${recipient}`, error instanceof Error ? error.stack : String(error), ); throw error; // DkvService handles retries with exponential backoff } ``` **Dynamic SMTP transport pattern** (RESEARCH.md Pattern 6): ```typescript const transport = nodemailer.createTransport({ host: smtpConfig.host, port: smtpConfig.port, secure: smtpConfig.encryption === 'ssl-tls', requireTLS: smtpConfig.encryption === 'starttls', auth: smtpConfig.username ? { user: smtpConfig.username, pass: decryptedPassword } : undefined, }); ``` --- ### `apps/api/src/dkv/providers/inbox-provider.interface.ts` (interface) **Analog:** `apps/api/src/calendar/calendar.service.ts` — `CalendarProvider` interface (lines 33–44) ```typescript // Mirror of CalendarProvider pattern — same structure, different method names export interface InboxEmail { uid: number | string; messageId: string; subject: string; from: string; date: Date; attachments: InboxAttachment[]; } export interface InboxAttachment { filename: string; contentType: string; buffer: Buffer; } export interface InboxProvider { fetchPdfAttachments(config: InboxConfig): Promise; testConnection(config: InboxConfig): Promise; } ``` --- ### `apps/api/src/dkv/providers/exchange-inbox.provider.ts` (provider, request-response) **Analog:** `apps/api/src/calendar/providers/exchange.provider.ts` (full file, 228 lines) **Class shell + Logger pattern** (exchange.provider.ts lines 1–48): ```typescript import { Injectable, Logger } from '@nestjs/common'; import { InboxEmail, InboxProvider } from './inbox-provider.interface'; @Injectable() export class ExchangeInboxProvider implements InboxProvider { private readonly logger = new Logger(ExchangeInboxProvider.name); async fetchPdfAttachments(config: InboxConfig): Promise { try { return await this.fetchViaEws(config); } catch (error) { this.logger.error( `EWS inbox fetch failed: ${(error as Error).message}`, // T-05-13: never include credentials in error ); return []; } } } ``` **EWS dynamic import pattern** (exchange.provider.ts lines 154–155): ```typescript const ews: any = await import('ews-javascript-api'); const service = new ews.ExchangeService(ews.ExchangeVersion.Exchange2013); service.Url = new ews.Uri(config.host); service.Credentials = new ews.WebCredentials(config.username, config.password); ``` **Critical difference from ExchangeProvider:** Use `ews.WellKnownFolderName.Inbox` with `service.FindItems()` and `ews.EmailMessage` — NOT `FindAppointments` / `CalendarView` (Pitfall 6). --- ### `apps/api/src/dkv/providers/imap.provider.ts` (provider, file-I/O) **Analog:** `apps/api/src/calendar/providers/caldav.provider.ts` (role-match — async provider shell) **Class shell:** ```typescript import { Injectable, Logger } from '@nestjs/common'; import { ImapFlow } from 'imapflow'; import { InboxEmail, InboxProvider } from './inbox-provider.interface'; @Injectable() export class ImapProvider implements InboxProvider { private readonly logger = new Logger(ImapProvider.name); // ... } ``` **Critical imapflow pattern** (RESEARCH.md Pattern 2 + Pitfall 1): ```typescript // NEVER call download() inside fetch() async iterator — causes IMAP deadlock // Always: fetchAll() first → loop → download() const messages = await client.fetchAll( uids.join(','), { envelope: true, bodyStructure: true }, { uid: true }, ); for (const msg of messages) { const { content } = await client.download(String(msg.uid), partId, { uid: true }); } ``` **Logger suppression** (security — never log IMAP credentials): ```typescript const client = new ImapFlow({ // ... logger: false, // suppress verbose imapflow logs (contain credentials) }); ``` --- ### `apps/api/src/dkv/dto/dkv-config.dto.ts` (dto) **Analog:** `apps/api/src/calendar/dto/create-calendar-source.dto.ts` **Validation decorator pattern** (lines 1–47): ```typescript import { IsBoolean, IsEmail, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, Max, Min, } from 'class-validator'; export class DkvConfigDto { @IsIn(['imap', 'exchange']) protocol!: string; @IsOptional() @IsString() host?: string; @IsOptional() @IsInt() @Min(1) @Max(65535) port?: number; @IsIn(['none', 'starttls', 'ssl-tls']) encryption!: string; @IsOptional() @IsString() folder?: string; @IsOptional() @IsEmail() senderFilter?: string; // IsEmail() validator — Pitfall: credential injection @IsOptional() @IsEmail() exportRecipient?: string; @IsOptional() @IsInt() @Min(1) pollIntervalMin?: number; @IsOptional() @IsBoolean() isActive?: boolean; } ``` --- ### `apps/api/src/settings/settings.controller.ts` (controller, request-response) **Analog:** `apps/api/src/ldap/ldap.controller.ts` **Pattern:** Same admin-only guard, same tenant extraction. Routes: `GET /settings/smtp` and `PUT /settings/smtp`. ```typescript @Controller('settings') export class SettingsController { constructor(private readonly settingsService: SettingsService) {} @Get('smtp') @Roles(Role.ADMIN, Role.SUPER_ADMIN) async getSmtpConfig(@Req() req: any) { const tenantId = req.tenantId; if (!tenantId) throw new BadRequestException('No tenant context'); const config = await this.settingsService.getSmtpConfig(tenantId); // Never return decryptedPassword — return hasPassword boolean (T-05-09 pattern) return config ? { ...config, encryptedPassword: undefined, hasPassword: !!config.encryptedPassword } : null; } @Put('smtp') @Roles(Role.ADMIN, Role.SUPER_ADMIN) async saveSmtpConfig(@Req() req: any, @Body() dto: SmtpConfigDto) { const tenantId = req.tenantId; if (!tenantId) throw new BadRequestException('No tenant context'); return this.settingsService.saveSmtpConfig(tenantId, dto); } } ``` --- ### `apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx` (component, request-response) **Analog:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` **Full component pattern** (domaincheck/page.tsx lines 1–64): ```typescript 'use client'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; export default function DkvFleetPage() { const t = useTranslations('dkvFleet'); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); return (

{t('title')}

{t('description')}

{/* InvoiceHistoryTable + manual trigger button */}
); } ``` **Layout:** `mx-auto max-w-5xl space-y-6 p-6` — wider than domaincheck (27 vehicles × 5 columns needs more space). --- ### `apps/web/src/app/(portal)/modules/dkv-fleet/settings/page.tsx` (component, CRUD) **Analog:** `apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx` **Pattern** (calendar/page.tsx lines 1–21): ```typescript 'use client'; import { useTranslations } from 'next-intl'; import { InboxConfigForm } from './components/InboxConfigForm'; import { VehicleTable } from './components/VehicleTable'; import { CsvImportButton } from './components/CsvImportButton'; export default function DkvFleetSettingsPage() { const t = useTranslations('dkvFleet'); return (

{t('settingsTitle')}

); } ``` --- ### `apps/web/src/app/(portal)/settings/general/smtp/page.tsx` (component, CRUD) **Analog:** `apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx` This page lives in the settings section — uses `SettingsLayout` (already wraps the route). ```typescript 'use client'; import { useTranslations } from 'next-intl'; export default function SmtpSettingsPage() { const t = useTranslations('settings'); return (

{t('categorySmtp')}

{/* SmtpConfigForm component */}
); } ``` **Settings sidebar extension** — add to `apps/web/src/components/settings/settings-sidebar.tsx`: ```typescript // Extend items array — add new "Allgemein" category above "Dashboard" const generalItems = [ { label: t('categorySmtp'), href: '/settings/general/smtp' }, ]; ``` --- ## Shared Patterns ### Authentication / Authorization **Source:** `apps/api/src/ldap/ldap.controller.ts` lines 38–40 **Apply to:** All DKV and Settings controller handlers ```typescript @Roles(Role.ADMIN, Role.SUPER_ADMIN) ``` Global `JwtAuthGuard` is already applied in `main.ts`. `@Roles` decorator adds role check on top. --- ### Credential Encryption (AES-256-GCM) **Source:** `apps/api/src/calendar/crypto.service.ts` (full file, 75 lines) **Apply to:** `dkv.service.ts` (inbox creds), `settings.service.ts` (SMTP password) ```typescript // Inject CalendarCryptoService — it handles the key lifecycle constructor(private readonly crypto: CalendarCryptoService) {} // Encrypt at save time const encrypted = this.crypto.encrypt(JSON.stringify({ username, password })); // Decrypt at use time — NEVER log result (T-05-13) const creds = JSON.parse(this.crypto.decrypt(encrypted)); ``` --- ### Tenant Context Extraction **Source:** `apps/api/src/ldap/ldap.controller.ts` lines 40–47 **Apply to:** All DKV and Settings controller handlers ```typescript const tenantId = req.tenantId; if (!tenantId) { throw new BadRequestException('No tenant context'); } ``` --- ### Generic Error Logging (no credential exposure) **Source:** `apps/api/src/calendar/providers/exchange.provider.ts` lines 42–48 **Apply to:** `dkv-mail.service.ts`, `imap.provider.ts`, `exchange-inbox.provider.ts`, `dkv-scheduler.service.ts` ```typescript this.logger.error( `Operation failed for tenant ${tenantId}: ${(error as Error).message}`, // T-05-13: message only — never include stack with potential credential details ); ``` --- ### NestJS Logger **Source:** `apps/api/src/calendar/providers/exchange.provider.ts` line 15 **Apply to:** All new `@Injectable()` services and providers ```typescript private readonly logger = new Logger(ClassName.name); ``` --- ### Safe API Response (no encrypted fields) **Source:** `apps/api/src/calendar/calendar.service.ts` lines 50–67 **Apply to:** `dkv.service.ts` (config), `settings.service.ts` (SMTP config) Define a `const X_SAFE_SELECT` Prisma select object that explicitly excludes `encryptedInboxCreds` / `encryptedPassword`. Never return these fields to the frontend. --- ### Frontend `'use client'` + `useTranslations` shell **Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` lines 1–7 **Apply to:** All new Next.js page and component files ```typescript 'use client'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; ``` --- ### Frontend Card Layout **Source:** `apps/web/src/app/(portal)/modules/domaincheck/page.tsx` lines 40–64 **Apply to:** `dkv-fleet/page.tsx`, settings form components ```typescript
{/* content */}
``` --- ## No Analog Found | File | Role | Data Flow | Reason | |------|------|-----------|--------| | `apps/api/src/dkv/dkv-export.service.ts` | service | file-I/O | No file-writing service exists in codebase (first file-I/O service) | | `apps/api/src/dkv/dkv-parser.service.ts` | service | transform | No binary-parsing / regex transform service exists (novel pattern) | For these files, use RESEARCH.md Patterns 3–5 (pdf-parse v2, DKV regex, SheetJS) as primary reference. Wrap in standard `@Injectable()` NestJS service shell from the shared pattern above. --- ## AppModule Modification **File:** `apps/api/src/app.module.ts` **Required change:** Add `ScheduleModule.forRoot()` (NOT imported yet — confirmed by codebase grep) and `DkvModule`: ```typescript import { ScheduleModule } from '@nestjs/schedule'; import { DkvModule } from './dkv/dkv.module'; import { SettingsModule } from './settings/settings.module'; @Module({ imports: [ // ... existing imports ... ScheduleModule.forRoot(), // prerequisite for DkvSchedulerService (Pitfall 4) DkvModule, SettingsModule, ], }) ``` --- ## Metadata **Analog search scope:** `apps/api/src/`, `apps/web/src/app/(portal)/` **Files scanned:** 21 existing source files **Pattern extraction date:** 2026-06-26