import { Body, Controller, Headers, HttpException, Logger, Param, Post } from '@nestjs/common'; import { PlatformGraphqlService } from '../platform/platform-graphql.service'; import { AiEnrichmentService } from '../ai/ai-enrichment.service'; import { CallerResolutionService } from '../caller/caller-resolution.service'; // POST /api/lead/:id/enrich // // Force re-generation of a lead's AI summary + suggested action. Used by // the call-desk appointment/enquiry forms when the agent explicitly edits // the caller's name — the previously-generated summary was built against // the stale identity, so we discard it and run the enrichment prompt // again with the corrected name. // // Optional body: `{ phone?: string }` — when provided, also invalidates // the Redis caller-resolution cache for that phone so the NEXT incoming // call from the same number picks up fresh data from the platform // instead of the stale cached entry. // // This is distinct from the cache-miss enrichment path in // call-lookup.controller.ts `POST /api/call/lookup` which only runs // enrichment when `lead.aiSummary` is null. That path is fine for // first-time lookups; this one is for explicit "the old summary is // wrong, regenerate it" triggers. @Controller('api/lead') export class LeadEnrichController { private readonly logger = new Logger(LeadEnrichController.name); constructor( private readonly platform: PlatformGraphqlService, private readonly ai: AiEnrichmentService, private readonly callerResolution: CallerResolutionService, ) {} @Post(':id/enrich') async enrichLead( @Param('id') leadId: string, @Body() body: { phone?: string }, @Headers('authorization') authHeader: string, ) { if (!authHeader) throw new HttpException('Authorization required', 401); if (!leadId) throw new HttpException('leadId required', 400); this.logger.log(`Force-enriching lead ${leadId}`); // 1. Fetch fresh lead from platform (with the staging-aligned // field names — see findLeadByIdWithToken comment). let lead: any; try { lead = await this.platform.findLeadByIdWithToken(leadId, authHeader); } catch (err) { this.logger.error(`[LEAD-ENRICH] Lead fetch failed for ${leadId}: ${err}`); throw new HttpException(`Lead fetch failed: ${(err as Error).message}`, 500); } if (!lead) { throw new HttpException(`Lead not found: ${leadId}`, 404); } // 2. Fetch recent activities so the prompt has conversation context. let activities: any[] = []; try { activities = await this.platform.getLeadActivitiesWithToken(leadId, authHeader, 5); } catch (err) { // Non-fatal — enrichment just has less context. this.logger.warn(`[LEAD-ENRICH] Activity fetch failed: ${err}`); } // 3. Run enrichment. LeadContext uses the legacy `leadStatus`/ // `leadSource` internal names even though the platform now // exposes them as `status`/`source` — we just map across. const enrichment = await this.ai.enrichLead({ firstName: lead.contactName?.firstName ?? undefined, lastName: lead.contactName?.lastName ?? undefined, leadSource: lead.source ?? undefined, interestedService: lead.interestedService ?? undefined, leadStatus: lead.status ?? undefined, contactAttempts: lead.contactAttempts ?? undefined, createdAt: lead.createdAt, activities: activities.map((a) => ({ activityType: a.activityType ?? '', summary: a.summary ?? '', })), }); // 4. Persist the new summary back to the lead. try { await this.platform.updateLeadWithToken( leadId, { aiSummary: enrichment.aiSummary, aiSuggestedAction: enrichment.aiSuggestedAction, }, authHeader, ); } catch (err) { this.logger.error(`[LEAD-ENRICH] Failed to persist enrichment for ${leadId}: ${err}`); throw new HttpException( `Failed to persist enrichment: ${(err as Error).message}`, 500, ); } // Caller resolution no longer caches — every resolve() hits the // platform fresh via an indexed phone filter. No invalidation // needed after enrichment. this.logger.log(`[LEAD-ENRICH] Lead ${leadId} enriched successfully`); return { leadId, aiSummary: enrichment.aiSummary, aiSuggestedAction: enrichment.aiSuggestedAction, }; } }