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 { 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 = {}; 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( '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, 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 { const key = NORTH_DATA_KEY(tenantId); const raw = await this.redis.get(key); if (!raw) { const envKey = this.config.get('NORTH_DATA_API_KEY'); return { apiKey: envKey ?? null, enabled: !!envKey, }; } return JSON.parse(raw) as NorthDataSettings; } async updateNorthDataSettings( tenantId: string, dto: UpdateNorthDataSettingsDto, ): Promise { 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('NORTH_DATA_API_KEY'); if (!envKey) return 'unconfigured'; try { const baseUrl = this.config.get( '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'; } } }