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>
26 KiB
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):
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):
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:
@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:
const tenantId = req.tenantId;
if (!tenantId) {
throw new BadRequestException('No tenant context');
}
Core routes pattern:
@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:
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):
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):
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):
// 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):
} 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):
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):
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class DkvParserService {
private readonly logger = new Logger(DkvParserService.name);
async parsePdf(buffer: Buffer): Promise<DkvVehicleBlock[]> {
// 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:
import { PDFParse } from 'pdf-parse';
async function extractPdfText(buffer: Buffer): Promise<string> {
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):
const vehicleBlockPattern =
/VEHICLE:\s+(\S+)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g;
German number parsing (mandatory for Kilometerstand/Menge):
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):
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):
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:
private processing = false;
async processInbox(): Promise<void> {
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):
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):
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):
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)
// 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<InboxEmail[]>;
testConnection(config: InboxConfig): Promise<boolean>;
}
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):
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<InboxEmail[]> {
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):
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:
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):
// 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):
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):
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.
@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):
'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<string | null>(null);
return (
<div className="mx-auto max-w-5xl space-y-6 p-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="text-sm text-muted-foreground mt-1">{t('description')}</p>
</div>
<div className="rounded-lg border border-border bg-card p-6 shadow-sm space-y-4">
{/* InvoiceHistoryTable + manual trigger button */}
</div>
</div>
);
}
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):
'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 (
<div>
<h1 className="mb-6 text-lg font-semibold text-foreground">
{t('settingsTitle')}
</h1>
<InboxConfigForm />
<VehicleTable />
</div>
);
}
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).
'use client';
import { useTranslations } from 'next-intl';
export default function SmtpSettingsPage() {
const t = useTranslations('settings');
return (
<div>
<h1 className="mb-6 text-lg font-semibold text-foreground">
{t('categorySmtp')}
</h1>
{/* SmtpConfigForm component */}
</div>
);
}
Settings sidebar extension — add to apps/web/src/components/settings/settings-sidebar.tsx:
// 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
@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)
// 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
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
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
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
'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
<div className="rounded-lg border border-border bg-card p-6 shadow-sm space-y-4">
{/* content */}
</div>
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:
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