mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: add Platform GraphQL client service for lead lookup and CRUD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
|
import { PlatformModule } from './platform/platform.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -8,6 +9,7 @@ import configuration from './config/configuration';
|
|||||||
load: [configuration],
|
load: [configuration],
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
|
PlatformModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
132
src/platform/platform-graphql.service.ts
Normal file
132
src/platform/platform-graphql.service.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
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')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async query<T>(query: string, variables?: Record<string, any>): Promise<T> {
|
||||||
|
const response = await axios.post(
|
||||||
|
this.graphqlUrl,
|
||||||
|
{ query, variables },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/platform/platform.module.ts
Normal file
8
src/platform/platform.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PlatformGraphqlService } from './platform-graphql.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PlatformGraphqlService],
|
||||||
|
exports: [PlatformGraphqlService],
|
||||||
|
})
|
||||||
|
export class PlatformModule {}
|
||||||
71
src/platform/platform.types.ts
Normal file
71
src/platform/platform.types.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export type LeadNode = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
contactName: { firstName: string; lastName: string } | null;
|
||||||
|
contactPhone: { number: string; callingCode: string }[] | null;
|
||||||
|
contactEmail: { address: string }[] | null;
|
||||||
|
leadSource: string | null;
|
||||||
|
leadStatus: string | null;
|
||||||
|
interestedService: string | null;
|
||||||
|
assignedAgent: string | null;
|
||||||
|
campaignId: string | null;
|
||||||
|
adId: string | null;
|
||||||
|
contactAttempts: number | null;
|
||||||
|
spamScore: number | null;
|
||||||
|
isSpam: boolean | null;
|
||||||
|
aiSummary: string | null;
|
||||||
|
aiSuggestedAction: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LeadActivityNode = {
|
||||||
|
id: string;
|
||||||
|
activityType: string | null;
|
||||||
|
summary: string | null;
|
||||||
|
occurredAt: string | null;
|
||||||
|
performedBy: string | null;
|
||||||
|
channel: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CallNode = {
|
||||||
|
id: string;
|
||||||
|
callDirection: string | null;
|
||||||
|
callStatus: string | null;
|
||||||
|
disposition: string | null;
|
||||||
|
agentName: string | null;
|
||||||
|
startedAt: string | null;
|
||||||
|
endedAt: string | null;
|
||||||
|
durationSeconds: number | null;
|
||||||
|
leadId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateCallInput = {
|
||||||
|
callDirection: string;
|
||||||
|
callStatus: string;
|
||||||
|
callerNumber?: { number: string; callingCode: string }[];
|
||||||
|
agentName?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
endedAt?: string;
|
||||||
|
durationSeconds?: number;
|
||||||
|
disposition?: string;
|
||||||
|
callNotes?: string;
|
||||||
|
leadId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateLeadActivityInput = {
|
||||||
|
activityType: string;
|
||||||
|
summary: string;
|
||||||
|
occurredAt: string;
|
||||||
|
performedBy: string;
|
||||||
|
channel: string;
|
||||||
|
durationSeconds?: number;
|
||||||
|
outcome?: string;
|
||||||
|
leadId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateLeadInput = {
|
||||||
|
leadStatus?: string;
|
||||||
|
lastContactedAt?: string;
|
||||||
|
aiSummary?: string;
|
||||||
|
aiSuggestedAction?: string;
|
||||||
|
contactAttempts?: number;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user