import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; import type { LeadNode, LeadActivityNode, CreateCallInput, CreateLeadActivityInput, UpdateLeadInput } from './platform.types'; @Injectable() export class PlatformGraphqlService { private readonly graphqlUrl: string; private readonly apiKey: string; constructor(private config: ConfigService) { this.graphqlUrl = config.get('platform.graphqlUrl')!; this.apiKey = config.get('platform.apiKey')!; } // Server-to-server query using API key private async query(query: string, variables?: Record): Promise { return this.queryWithAuth(query, variables, `Bearer ${this.apiKey}`); } // Query using a passed-through auth header (user JWT) private async queryWithAuth(query: string, variables: Record | undefined, authHeader: string): Promise { const response = await axios.post( this.graphqlUrl, { query, variables }, { headers: { 'Content-Type': 'application/json', 'Authorization': authHeader, }, }, ); if (response.data.errors) { throw new Error(`GraphQL error: ${JSON.stringify(response.data.errors)}`); } return response.data.data; } async findLeadByPhone(phone: string): Promise { // Note: The exact filter syntax for PHONES fields depends on the platform // This queries leads and filters client-side by phone number const data = await this.query<{ leads: { edges: { node: LeadNode }[] } }>( `query FindLeads($first: Int) { leads(first: $first, orderBy: { createdAt: DescNullsLast }) { edges { node { id createdAt contactName { firstName lastName } contactPhone { number callingCode } contactEmail { address } leadSource leadStatus interestedService assignedAgent campaignId adId contactAttempts spamScore isSpam aiSummary aiSuggestedAction } } } }`, { first: 100 }, ); // Client-side phone matching (strip non-digits for comparison) const normalizedPhone = phone.replace(/\D/g, ''); return data.leads.edges.find(edge => { const leadPhones = edge.node.contactPhone ?? []; return leadPhones.some(p => p.number.replace(/\D/g, '').endsWith(normalizedPhone) || normalizedPhone.endsWith(p.number.replace(/\D/g, ''))); })?.node ?? null; } async findLeadById(id: string): Promise { const data = await this.query<{ lead: LeadNode }>( `query FindLead($id: ID!) { lead(id: $id) { id createdAt contactName { firstName lastName } contactPhone { number callingCode } contactEmail { address } leadSource leadStatus interestedService assignedAgent campaignId adId contactAttempts spamScore isSpam aiSummary aiSuggestedAction } }`, { id }, ); return data.lead; } async updateLead(id: string, input: UpdateLeadInput): Promise { const data = await this.query<{ updateLead: LeadNode }>( `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id leadStatus aiSummary aiSuggestedAction } }`, { id, data: input }, ); return data.updateLead; } async createCall(input: CreateCallInput): Promise<{ id: string }> { const data = await this.query<{ createCall: { id: string } }>( `mutation CreateCall($data: CallCreateInput!) { createCall(data: $data) { id } }`, { data: input }, ); return data.createCall; } async createLeadActivity(input: CreateLeadActivityInput): Promise<{ id: string }> { const data = await this.query<{ createLeadActivity: { id: string } }>( `mutation CreateLeadActivity($data: LeadActivityCreateInput!) { createLeadActivity(data: $data) { id } }`, { data: input }, ); return data.createLeadActivity; } // --- Token passthrough versions (for user-driven requests) --- async findLeadByPhoneWithToken(phone: string, authHeader: string): Promise { const normalizedPhone = phone.replace(/\D/g, ''); const last10 = normalizedPhone.slice(-10); const data = await this.queryWithAuth<{ leads: { edges: { node: LeadNode }[] } }>( `query FindLeads($first: Int) { leads(first: $first, orderBy: [{ createdAt: DescNullsLast }]) { edges { node { id createdAt contactName { firstName lastName } contactPhone { number callingCode } contactEmail { address } leadSource leadStatus interestedService assignedAgent campaignId adId contactAttempts spamScore isSpam aiSummary aiSuggestedAction } } } }`, { first: 200 }, authHeader, ); // Client-side phone matching return data.leads.edges.find(edge => { const phones = edge.node.contactPhone ?? []; if (Array.isArray(phones)) { return phones.some((p: any) => { const num = (p.number ?? p.primaryPhoneNumber ?? '').replace(/\D/g, ''); return num.endsWith(last10) || last10.endsWith(num); }); } // Handle single phone object const num = ((phones as any).primaryPhoneNumber ?? (phones as any).number ?? '').replace(/\D/g, ''); return num.endsWith(last10) || last10.endsWith(num); })?.node ?? null; } async getLeadActivitiesWithToken(leadId: string, authHeader: string, limit = 5): Promise { const data = await this.queryWithAuth<{ leadActivities: { edges: { node: LeadActivityNode }[] } }>( `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { leadActivities(filter: $filter, first: $first, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node { id activityType summary occurredAt performedBy channel } } } }`, { filter: { leadId: { eq: leadId } }, first: limit }, authHeader, ); return data.leadActivities.edges.map(e => e.node); } async updateLeadWithToken(id: string, input: UpdateLeadInput, authHeader: string): Promise { const data = await this.queryWithAuth<{ updateLead: LeadNode }>( `mutation UpdateLead($id: ID!, $data: LeadUpdateInput!) { updateLead(id: $id, data: $data) { id leadStatus aiSummary aiSuggestedAction } }`, { id, data: input }, authHeader, ); return data.updateLead; } // --- Server-to-server versions (for webhooks, background jobs) --- async getLeadActivities(leadId: string, limit = 3): Promise { const data = await this.query<{ leadActivities: { edges: { node: LeadActivityNode }[] } }>( `query GetLeadActivities($filter: LeadActivityFilterInput, $first: Int) { leadActivities(filter: $filter, first: $first, orderBy: { occurredAt: DescNullsLast }) { edges { node { id activityType summary occurredAt performedBy channel } } } }`, { filter: { leadId: { eq: leadId } }, first: limit }, ); return data.leadActivities.edges.map(e => e.node); } }