From 5b35c65e6e31266ea7c50fd91679f0904dd095a5 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 17 Mar 2026 09:04:00 +0530 Subject: [PATCH] feat: add Platform GraphQL client service for lead lookup and CRUD Co-Authored-By: Claude Sonnet 4.6 --- src/app.module.ts | 2 + src/platform/.gitkeep | 0 src/platform/platform-graphql.service.ts | 132 +++++++++++++++++++++++ src/platform/platform.module.ts | 8 ++ src/platform/platform.types.ts | 71 ++++++++++++ 5 files changed, 213 insertions(+) delete mode 100644 src/platform/.gitkeep create mode 100644 src/platform/platform-graphql.service.ts create mode 100644 src/platform/platform.module.ts create mode 100644 src/platform/platform.types.ts diff --git a/src/app.module.ts b/src/app.module.ts index 2a9c17e..5917928 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import configuration from './config/configuration'; +import { PlatformModule } from './platform/platform.module'; @Module({ imports: [ @@ -8,6 +9,7 @@ import configuration from './config/configuration'; load: [configuration], isGlobal: true, }), + PlatformModule, ], }) export class AppModule {} diff --git a/src/platform/.gitkeep b/src/platform/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/platform/platform-graphql.service.ts b/src/platform/platform-graphql.service.ts new file mode 100644 index 0000000..fa67f52 --- /dev/null +++ b/src/platform/platform-graphql.service.ts @@ -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('platform.graphqlUrl')!; + this.apiKey = config.get('platform.apiKey')!; + } + + private async query(query: string, variables?: Record): Promise { + 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 { + // 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; + } + + 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); + } +} diff --git a/src/platform/platform.module.ts b/src/platform/platform.module.ts new file mode 100644 index 0000000..08164f9 --- /dev/null +++ b/src/platform/platform.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PlatformGraphqlService } from './platform-graphql.service'; + +@Module({ + providers: [PlatformGraphqlService], + exports: [PlatformGraphqlService], +}) +export class PlatformModule {} diff --git a/src/platform/platform.types.ts b/src/platform/platform.types.ts new file mode 100644 index 0000000..490d5f3 --- /dev/null +++ b/src/platform/platform.types.ts @@ -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; +};