Files
helix-engage-server/src/platform/platform-graphql.service.ts

213 lines
8.6 KiB
TypeScript

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<string>('platform.graphqlUrl')!;
this.apiKey = config.get<string>('platform.apiKey')!;
}
// Server-to-server query using API key
private async query<T>(query: string, variables?: Record<string, any>): Promise<T> {
return this.queryWithAuth<T>(query, variables, `Bearer ${this.apiKey}`);
}
// Query using a passed-through auth header (user JWT)
private async queryWithAuth<T>(query: string, variables: Record<string, any> | undefined, authHeader: string): Promise<T> {
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<LeadNode | null> {
// 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<LeadNode | null> {
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<LeadNode> {
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<LeadNode | null> {
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<LeadActivityNode[]> {
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<LeadNode> {
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<LeadActivityNode[]> {
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);
}
}