mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
- Phase 2.3 Forecast: probability-Feld in PipelineStage, GET /crm/deals/forecast Endpoint - Phase 2.2 Import: ImportModule mit preview/execute/history Endpoints (CSV, XLSX, vCard) - Phase 2.4 Enrichment: EnrichmentModule mit /enrich + /settings/integrations/north-data - Contracts: ContractsModule mit CRUD + File-Upload Endpoints (Multer, max 25MB) - Migrations: 20260312_contract_files, 20260312_phase23_forecast - docker-compose.crm.yml: uploads Volume für Vertragsdokumente Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
307 lines
9.8 KiB
TypeScript
307 lines
9.8 KiB
TypeScript
import {
|
|
Injectable,
|
|
Logger,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { HttpService } from '@nestjs/axios';
|
|
import { firstValueFrom } from 'rxjs';
|
|
import { CrmPrismaService } from '../prisma/crm-prisma.service';
|
|
import { RedisService } from '../redis/redis.service';
|
|
import {
|
|
EnrichmentResponse,
|
|
EnrichmentSuggestion,
|
|
} from './dto/enrich-response.dto';
|
|
import {
|
|
NorthDataSettings,
|
|
UpdateNorthDataSettingsDto,
|
|
} from './dto/enrichment-settings.dto';
|
|
|
|
const NORTH_DATA_KEY = (tenantId: string) =>
|
|
`crm:${tenantId}:integrations:north_data`;
|
|
|
|
@Injectable()
|
|
export class EnrichmentService {
|
|
private readonly logger = new Logger(EnrichmentService.name);
|
|
|
|
constructor(
|
|
private readonly prisma: CrmPrismaService,
|
|
private readonly redis: RedisService,
|
|
private readonly config: ConfigService,
|
|
private readonly httpService: HttpService,
|
|
) {}
|
|
|
|
// --------------------------------------------------------
|
|
// Unternehmen anreichern (Suggestion-Only)
|
|
// --------------------------------------------------------
|
|
|
|
async enrichCompany(
|
|
tenantId: string,
|
|
companyId: string,
|
|
): Promise<EnrichmentResponse> {
|
|
const company = await this.prisma.company.findFirst({
|
|
where: { id: companyId, tenantId },
|
|
});
|
|
if (!company) {
|
|
throw new NotFoundException('Unternehmen nicht gefunden');
|
|
}
|
|
|
|
const warnings: string[] = [];
|
|
const sources: string[] = [];
|
|
const suggestions: Record<string, EnrichmentSuggestion> = {};
|
|
|
|
const northDataSettings = await this.getNorthDataSettings(tenantId);
|
|
|
|
// Parallele Abfragen
|
|
const [registerResult, northDataResult] = await Promise.allSettled([
|
|
this.queryUnternehmensregister(company.name, company.city ?? undefined),
|
|
northDataSettings.enabled && northDataSettings.apiKey
|
|
? this.queryNorthData(company.name, northDataSettings.apiKey)
|
|
: Promise.resolve(null),
|
|
]);
|
|
|
|
// Unternehmensregister.de Ergebnisse verarbeiten
|
|
if (registerResult.status === 'fulfilled' && registerResult.value) {
|
|
sources.push('unternehmensregister.de');
|
|
const data = registerResult.value;
|
|
|
|
this.addSuggestion(suggestions, 'tradeRegisterNumber', company.tradeRegisterNumber, data.registerNumber, 'unternehmensregister.de');
|
|
this.addSuggestion(suggestions, 'registerCourt', company.registerCourt, data.registerCourt, 'unternehmensregister.de');
|
|
this.addSuggestion(suggestions, 'street', company.street, data.street, 'unternehmensregister.de');
|
|
this.addSuggestion(suggestions, 'zip', company.zip, data.zip, 'unternehmensregister.de');
|
|
this.addSuggestion(suggestions, 'city', company.city, data.city, 'unternehmensregister.de');
|
|
} else if (registerResult.status === 'rejected') {
|
|
warnings.push(`Unternehmensregister: ${String(registerResult.reason)}`);
|
|
}
|
|
|
|
// North Data Ergebnisse verarbeiten
|
|
if (northDataResult.status === 'fulfilled' && northDataResult.value) {
|
|
sources.push('northdata.de');
|
|
const data = northDataResult.value;
|
|
|
|
this.addSuggestion(suggestions, 'name', company.name, data.name, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'vatId', company.vatId, data.vatId, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'website', company.website, data.website, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'phone', company.phone, data.phone, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'street', company.street, data.street, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'zip', company.zip, data.zip, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'city', company.city, data.city, 'northdata.de');
|
|
this.addSuggestion(suggestions, 'industry', company.industry, data.industry, 'northdata.de');
|
|
} else if (northDataResult.status === 'rejected' && northDataSettings.enabled) {
|
|
warnings.push(`North Data: ${String(northDataResult.reason)}`);
|
|
} else if (!northDataSettings.enabled) {
|
|
warnings.push('North Data Integration ist nicht aktiviert.');
|
|
}
|
|
|
|
if (sources.length === 0) {
|
|
warnings.push('Keine Datenquelle konnte abgefragt werden.');
|
|
}
|
|
|
|
return {
|
|
companyId,
|
|
companyName: company.name,
|
|
sources,
|
|
suggestions,
|
|
enrichedAt: new Date().toISOString(),
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Unternehmensregister.de Abfrage (kostenfrei)
|
|
// --------------------------------------------------------
|
|
|
|
private async queryUnternehmensregister(
|
|
companyName: string,
|
|
city?: string,
|
|
): Promise<{
|
|
registerNumber: string | null;
|
|
registerCourt: string | null;
|
|
street: string | null;
|
|
zip: string | null;
|
|
city: string | null;
|
|
} | null> {
|
|
try {
|
|
const searchUrl = 'https://www.unternehmensregister.de/trxweb/api/search';
|
|
|
|
const response = await firstValueFrom(
|
|
this.httpService.get(searchUrl, {
|
|
params: {
|
|
keyword: companyName,
|
|
...(city ? { location: city } : {}),
|
|
},
|
|
timeout: 10000,
|
|
}),
|
|
);
|
|
|
|
const data = response.data;
|
|
|
|
if (data && typeof data === 'object') {
|
|
return {
|
|
registerNumber: data.registerNumber ?? null,
|
|
registerCourt: data.registerCourt ?? null,
|
|
street: data.street ?? null,
|
|
zip: data.zip ?? null,
|
|
city: data.city ?? null,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (err) {
|
|
this.logger.warn(
|
|
`Unternehmensregister-Abfrage fehlgeschlagen fuer "${companyName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
);
|
|
throw new Error('Unternehmensregister nicht erreichbar');
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// North Data API Abfrage (kostenpflichtig)
|
|
// --------------------------------------------------------
|
|
|
|
private async queryNorthData(
|
|
companyName: string,
|
|
apiKey: string,
|
|
): Promise<{
|
|
name: string | null;
|
|
vatId: string | null;
|
|
website: string | null;
|
|
phone: string | null;
|
|
street: string | null;
|
|
zip: string | null;
|
|
city: string | null;
|
|
industry: string | null;
|
|
} | null> {
|
|
try {
|
|
const baseUrl = this.config.get<string>(
|
|
'NORTH_DATA_API_URL',
|
|
'https://www.northdata.de/_api',
|
|
);
|
|
|
|
const response = await firstValueFrom(
|
|
this.httpService.get(`${baseUrl}/company/v1/company`, {
|
|
params: {
|
|
name: companyName,
|
|
address: 'DE',
|
|
},
|
|
headers: {
|
|
'X-Api-Key': apiKey,
|
|
},
|
|
timeout: 15000,
|
|
}),
|
|
);
|
|
|
|
const data = response.data;
|
|
|
|
if (data && typeof data === 'object') {
|
|
return {
|
|
name: data.name?.current ?? null,
|
|
vatId: data.vatId ?? null,
|
|
website: data.website ?? null,
|
|
phone: data.phone ?? null,
|
|
street: data.address?.street ?? null,
|
|
zip: data.address?.postalCode ?? null,
|
|
city: data.address?.city ?? null,
|
|
industry: data.industry?.description ?? null,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (err) {
|
|
this.logger.warn(
|
|
`North Data Abfrage fehlgeschlagen fuer "${companyName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
);
|
|
throw new Error('North Data API nicht erreichbar');
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Helfer: Suggestion hinzufuegen (nur wenn suggested != current)
|
|
// --------------------------------------------------------
|
|
|
|
private addSuggestion(
|
|
suggestions: Record<string, EnrichmentSuggestion>,
|
|
field: string,
|
|
current: string | null | undefined,
|
|
suggested: string | null | undefined,
|
|
source: string,
|
|
): void {
|
|
if (!suggested) return;
|
|
|
|
const currentVal = current ?? null;
|
|
const suggestedVal = suggested;
|
|
|
|
// Nur hinzufuegen wenn current leer oder verschieden
|
|
if (currentVal !== suggestedVal) {
|
|
// Erste Quelle gewinnt bei Duplikaten
|
|
if (!suggestions[field]) {
|
|
suggestions[field] = {
|
|
current: currentVal,
|
|
suggested: suggestedVal,
|
|
source,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// North Data Settings (Redis)
|
|
// --------------------------------------------------------
|
|
|
|
async getNorthDataSettings(tenantId: string): Promise<NorthDataSettings> {
|
|
const key = NORTH_DATA_KEY(tenantId);
|
|
const raw = await this.redis.get(key);
|
|
|
|
if (!raw) {
|
|
const envKey = this.config.get<string>('NORTH_DATA_API_KEY');
|
|
return {
|
|
apiKey: envKey ?? null,
|
|
enabled: !!envKey,
|
|
};
|
|
}
|
|
|
|
return JSON.parse(raw) as NorthDataSettings;
|
|
}
|
|
|
|
async updateNorthDataSettings(
|
|
tenantId: string,
|
|
dto: UpdateNorthDataSettingsDto,
|
|
): Promise<NorthDataSettings> {
|
|
const current = await this.getNorthDataSettings(tenantId);
|
|
|
|
const updated: NorthDataSettings = {
|
|
apiKey: dto.apiKey !== undefined ? dto.apiKey : current.apiKey,
|
|
enabled: dto.enabled !== undefined ? dto.enabled : current.enabled,
|
|
};
|
|
|
|
const key = NORTH_DATA_KEY(tenantId);
|
|
await this.redis.set(key, JSON.stringify(updated));
|
|
|
|
return updated;
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Health Check
|
|
// --------------------------------------------------------
|
|
|
|
async isHealthy(): Promise<'up' | 'down' | 'unconfigured'> {
|
|
const envKey = this.config.get<string>('NORTH_DATA_API_KEY');
|
|
if (!envKey) return 'unconfigured';
|
|
|
|
try {
|
|
const baseUrl = this.config.get<string>(
|
|
'NORTH_DATA_API_URL',
|
|
'https://www.northdata.de/_api',
|
|
);
|
|
await firstValueFrom(
|
|
this.httpService.get(`${baseUrl}/health`, {
|
|
headers: { 'X-Api-Key': envKey },
|
|
timeout: 5000,
|
|
}),
|
|
);
|
|
return 'up';
|
|
} catch {
|
|
return 'down';
|
|
}
|
|
}
|
|
}
|