Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-PATTERNS.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

830 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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