docs(07): create phase 7 execution plans for DKV fleet module
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

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>
This commit is contained in:
2026-06-26 18:56:59 +02:00
parent e9f4f2dcad
commit de06794e67
9 changed files with 2180 additions and 19 deletions
@@ -0,0 +1,829 @@
# 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<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:
```typescript
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):
```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<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):
```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<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):
```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<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):
```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<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):
```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 (
<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).
```typescript
'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`:
```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
<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`:
```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