mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
Revert "AI Summary not showing appointments fix."
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This reverts commit 973614749b.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,7 +42,3 @@ lerna-debug.log*
|
||||
# Each environment mints its own HMAC-signed site key.
|
||||
data/widget.json
|
||||
data/widget-backups/
|
||||
.env.local
|
||||
.grepai/config.yaml
|
||||
.grepai/symbols.gob
|
||||
.grepai/symbols.gob.lock
|
||||
|
||||
@@ -15,17 +15,6 @@ type LeadContext = {
|
||||
contactAttempts?: number;
|
||||
createdAt?: string;
|
||||
campaignId?: string;
|
||||
patient?: {
|
||||
age?: number;
|
||||
type?: string; // 'new' | 'returning' | 'vip'
|
||||
hasRecords?: boolean;
|
||||
};
|
||||
upcomingAppointments?: Array<{
|
||||
scheduledAt?: string;
|
||||
doctorName?: string;
|
||||
appointmentType?: string;
|
||||
status?: string;
|
||||
}>;
|
||||
activities?: { activityType: string; summary: string }[];
|
||||
};
|
||||
|
||||
@@ -35,12 +24,8 @@ type EnrichmentResult = {
|
||||
};
|
||||
|
||||
const enrichmentSchema = z.object({
|
||||
aiSummary: z
|
||||
.string()
|
||||
.describe('1-2 sentence summary of who this lead is and their history'),
|
||||
aiSuggestedAction: z
|
||||
.string()
|
||||
.describe('5-10 word suggested action for the agent'),
|
||||
aiSummary: z.string().describe('1-2 sentence summary of who this lead is and their history'),
|
||||
aiSuggestedAction: z.string().describe('5-10 word suggested action for the agent'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
@@ -71,51 +56,28 @@ export class AiEnrichmentService {
|
||||
|
||||
try {
|
||||
const daysSince = lead.createdAt
|
||||
? Math.floor(
|
||||
(Date.now() - new Date(lead.createdAt).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)
|
||||
? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24))
|
||||
: 0;
|
||||
|
||||
const activitiesText = lead.activities?.length
|
||||
? lead.activities
|
||||
.map((a) => `- ${a.activityType}: ${a.summary}`)
|
||||
.join('\n')
|
||||
? lead.activities.map(a => `- ${a.activityType}: ${a.summary}`).join('\n')
|
||||
: 'No previous interactions';
|
||||
|
||||
const patientContext = lead.patient
|
||||
? `Age: ${lead.patient.age ?? 'Unknown'} | Type: ${lead.patient.type ?? 'Unknown'} | Prior Records: ${lead.patient.hasRecords ? 'Yes' : 'No'}`
|
||||
: 'No patient record linked';
|
||||
|
||||
const appointmentContext = lead.upcomingAppointments?.length
|
||||
? lead.upcomingAppointments
|
||||
.map(
|
||||
(a) =>
|
||||
`- ${a.scheduledAt ? new Date(a.scheduledAt).toLocaleDateString() : 'TBD'}: ${a.appointmentType ?? 'Appointment'} with ${a.doctorName ?? 'Doctor'} (${a.status ?? 'Scheduled'})`,
|
||||
)
|
||||
.join('\n')
|
||||
: 'No upcoming appointments';
|
||||
|
||||
const { object } = await generateObject({
|
||||
model: this.aiModel!,
|
||||
schema: enrichmentSchema,
|
||||
prompt: this.aiConfig.renderPrompt('leadEnrichment', {
|
||||
leadName:
|
||||
`${lead.firstName ?? 'Unknown'} ${lead.lastName ?? ''}`.trim(),
|
||||
leadName: `${lead.firstName ?? 'Unknown'} ${lead.lastName ?? ''}`.trim(),
|
||||
leadSource: lead.leadSource ?? 'Unknown',
|
||||
interestedService: lead.interestedService ?? 'Unknown',
|
||||
leadStatus: lead.leadStatus ?? 'Unknown',
|
||||
daysSince,
|
||||
contactAttempts: lead.contactAttempts ?? 0,
|
||||
patientContext,
|
||||
appointmentContext,
|
||||
activities: activitiesText,
|
||||
}),
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`,
|
||||
);
|
||||
this.logger.log(`AI enrichment generated for lead ${lead.firstName} ${lead.lastName}`);
|
||||
return object;
|
||||
} catch (error) {
|
||||
this.logger.error(`AI enrichment failed: ${error}`);
|
||||
@@ -125,16 +87,12 @@ export class AiEnrichmentService {
|
||||
|
||||
private fallbackEnrichment(lead: LeadContext): EnrichmentResult {
|
||||
const daysSince = lead.createdAt
|
||||
? Math.floor(
|
||||
(Date.now() - new Date(lead.createdAt).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)
|
||||
? Math.floor((Date.now() - new Date(lead.createdAt).getTime()) / (1000 * 60 * 60 * 24))
|
||||
: 0;
|
||||
|
||||
const attempts = lead.contactAttempts ?? 0;
|
||||
const service = lead.interestedService ?? 'general inquiry';
|
||||
const source =
|
||||
lead.leadSource?.replace(/_/g, ' ').toLowerCase() ?? 'unknown source';
|
||||
const source = lead.leadSource?.replace(/_/g, ' ').toLowerCase() ?? 'unknown source';
|
||||
|
||||
let summary: string;
|
||||
let action: string;
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Logger,
|
||||
Headers,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { Controller, Post, Body, Logger, Headers, HttpException } from '@nestjs/common';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
import { AiEnrichmentService } from '../ai/ai-enrichment.service';
|
||||
|
||||
@@ -40,51 +33,17 @@ export class CallLookupController {
|
||||
}
|
||||
|
||||
if (lead) {
|
||||
this.logger.log(
|
||||
`Matched lead: ${lead.id} — ${lead.contactName?.firstName} ${lead.contactName?.lastName}`,
|
||||
);
|
||||
this.logger.log(`Matched lead: ${lead.id} — ${lead.contactName?.firstName} ${lead.contactName?.lastName}`);
|
||||
|
||||
// Get recent activities
|
||||
try {
|
||||
activities = await this.platform.getLeadActivitiesWithToken(
|
||||
lead.id,
|
||||
authHeader,
|
||||
5,
|
||||
);
|
||||
activities = await this.platform.getLeadActivitiesWithToken(lead.id, authHeader, 5);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Activity fetch failed: ${err}`);
|
||||
}
|
||||
|
||||
// Fetch patient context if patientId exists
|
||||
let patientData = null;
|
||||
let upcomingAppointments: any[] = [];
|
||||
if (lead.patientId) {
|
||||
try {
|
||||
patientData = await this.platform.getPatientWithToken(
|
||||
lead.patientId,
|
||||
authHeader,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Patient fetch failed: ${err}`);
|
||||
}
|
||||
|
||||
if (patientData) {
|
||||
try {
|
||||
upcomingAppointments =
|
||||
await this.platform.getUpcomingAppointmentsWithToken(
|
||||
lead.patientId,
|
||||
authHeader,
|
||||
3,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Appointment fetch failed: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI enrichment if no existing summary
|
||||
// generate aiSummary everytime
|
||||
// if (!lead.aiSummary) {
|
||||
if (!lead.aiSummary) {
|
||||
try {
|
||||
const enrichment = await this.ai.enrichLead({
|
||||
firstName: lead.contactName?.firstName,
|
||||
@@ -94,20 +53,6 @@ export class CallLookupController {
|
||||
leadStatus: lead.leadStatus ?? undefined,
|
||||
contactAttempts: lead.contactAttempts ?? undefined,
|
||||
createdAt: lead.createdAt,
|
||||
patient: patientData
|
||||
? {
|
||||
age: patientData.dateOfBirth
|
||||
? Math.floor(
|
||||
(Date.now() -
|
||||
new Date(patientData.dateOfBirth).getTime()) /
|
||||
(1000 * 60 * 60 * 24 * 365.25),
|
||||
)
|
||||
: undefined,
|
||||
type: patientData.patientType,
|
||||
hasRecords: true,
|
||||
}
|
||||
: undefined,
|
||||
upcomingAppointments,
|
||||
activities: activities.map((a: any) => ({
|
||||
activityType: a.activityType ?? '',
|
||||
summary: a.summary ?? '',
|
||||
@@ -119,21 +64,17 @@ export class CallLookupController {
|
||||
|
||||
// Persist AI enrichment back to platform
|
||||
try {
|
||||
await this.platform.updateLeadWithToken(
|
||||
lead.id,
|
||||
{
|
||||
await this.platform.updateLeadWithToken(lead.id, {
|
||||
aiSummary: enrichment.aiSummary,
|
||||
aiSuggestedAction: enrichment.aiSuggestedAction,
|
||||
},
|
||||
authHeader,
|
||||
);
|
||||
}, authHeader);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Failed to persist AI enrichment: ${err}`);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`AI enrichment failed: ${err}`);
|
||||
}
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
this.logger.log(`No lead found for phone ${phone}`);
|
||||
}
|
||||
|
||||
@@ -153,16 +153,8 @@ Lead details:
|
||||
- Lead age: {{daysSince}} days
|
||||
- Contact attempts: {{contactAttempts}}
|
||||
|
||||
Patient Context:
|
||||
{{patientContext}}
|
||||
|
||||
Upcoming Appointments:
|
||||
{{appointmentContext}}
|
||||
|
||||
Recent activity:
|
||||
{{activities}}
|
||||
|
||||
Generate a 1-2 sentence summary of the lead and a 5-10 word suggested action for the agent.`;
|
||||
{{activities}}`;
|
||||
|
||||
const CALL_INSIGHT_DEFAULT = `You are a CRM assistant for {{hospitalName}}.
|
||||
Generate a brief, actionable insight about this lead based on their interaction history.
|
||||
@@ -212,22 +204,10 @@ export const DEFAULT_AI_PROMPTS: Record<AiActorKey, AiPromptConfig> = {
|
||||
'Website widget chat',
|
||||
'Patient-facing bot embedded on the hospital website. Handles general questions, finds doctors, suggests appointments.',
|
||||
[
|
||||
{
|
||||
key: 'hospitalName',
|
||||
description: 'Branded hospital display name from theme.json',
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
description: 'Visitor first name (or "there" if unknown)',
|
||||
},
|
||||
{
|
||||
key: 'branchContext',
|
||||
description: 'Pre-rendered branch-selection instructions block',
|
||||
},
|
||||
{
|
||||
key: 'knowledgeBase',
|
||||
description: 'Pre-rendered list of departments + doctors + clinics',
|
||||
},
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name from theme.json' },
|
||||
{ key: 'userName', description: 'Visitor first name (or "there" if unknown)' },
|
||||
{ key: 'branchContext', description: 'Pre-rendered branch-selection instructions block' },
|
||||
{ key: 'knowledgeBase', description: 'Pre-rendered list of departments + doctors + clinics' },
|
||||
],
|
||||
WIDGET_CHAT_DEFAULT,
|
||||
),
|
||||
@@ -236,18 +216,16 @@ export const DEFAULT_AI_PROMPTS: Record<AiActorKey, AiPromptConfig> = {
|
||||
'In-call assistant the CC agent uses to look up patient history, doctor details, and clinic info while on a call.',
|
||||
[
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name' },
|
||||
{
|
||||
key: 'knowledgeBase',
|
||||
description:
|
||||
'Pre-rendered hospital knowledge base (clinics, doctors, packages)',
|
||||
},
|
||||
{ key: 'knowledgeBase', description: 'Pre-rendered hospital knowledge base (clinics, doctors, packages)' },
|
||||
],
|
||||
CC_AGENT_HELPER_DEFAULT,
|
||||
),
|
||||
supervisorChat: promptDefault(
|
||||
'Supervisor assistant',
|
||||
'AI tools the supervisor uses to query agent performance, campaign stats, and SLA breaches.',
|
||||
[{ key: 'hospitalName', description: 'Branded hospital display name' }],
|
||||
[
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name' },
|
||||
],
|
||||
SUPERVISOR_CHAT_DEFAULT,
|
||||
),
|
||||
leadEnrichment: promptDefault(
|
||||
@@ -255,36 +233,21 @@ export const DEFAULT_AI_PROMPTS: Record<AiActorKey, AiPromptConfig> = {
|
||||
'Generates an AI summary + suggested action for a new inbound lead before the agent picks up.',
|
||||
[
|
||||
{ key: 'leadName', description: 'Lead first + last name' },
|
||||
{
|
||||
key: 'leadSource',
|
||||
description: 'Source channel (WHATSAPP, GOOGLE_ADS, etc.)',
|
||||
},
|
||||
{ key: 'leadSource', description: 'Source channel (WHATSAPP, GOOGLE_ADS, etc.)' },
|
||||
{ key: 'interestedService', description: 'What the lead enquired about' },
|
||||
{ key: 'leadStatus', description: 'Current lead status' },
|
||||
{ key: 'daysSince', description: 'Days since the lead was created' },
|
||||
{ key: 'contactAttempts', description: 'Prior contact attempts count' },
|
||||
{
|
||||
key: 'patientContext',
|
||||
description:
|
||||
'Patient age, type (new/returning), and whether they have prior records',
|
||||
},
|
||||
{
|
||||
key: 'appointmentContext',
|
||||
description:
|
||||
'Next 3 scheduled appointments (date, doctor, type, status)',
|
||||
},
|
||||
{
|
||||
key: 'activities',
|
||||
description:
|
||||
'Pre-rendered recent activity summary (last 5 interactions)',
|
||||
},
|
||||
{ key: 'activities', description: 'Pre-rendered recent activity summary' },
|
||||
],
|
||||
LEAD_ENRICHMENT_DEFAULT,
|
||||
),
|
||||
callInsight: promptDefault(
|
||||
'Post-call insight',
|
||||
'After each call, generates a 2-3 sentence summary + a single suggested next action for the lead record.',
|
||||
[{ key: 'hospitalName', description: 'Branded hospital display name' }],
|
||||
[
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name' },
|
||||
],
|
||||
CALL_INSIGHT_DEFAULT,
|
||||
),
|
||||
callAssist: promptDefault(
|
||||
@@ -292,11 +255,7 @@ export const DEFAULT_AI_PROMPTS: Record<AiActorKey, AiPromptConfig> = {
|
||||
'Real-time suggestions whispered to the CC agent during a call, based on the running transcript.',
|
||||
[
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name' },
|
||||
{
|
||||
key: 'context',
|
||||
description:
|
||||
'Pre-rendered call context (current lead, recent activities, available doctors)',
|
||||
},
|
||||
{ key: 'context', description: 'Pre-rendered call context (current lead, recent activities, available doctors)' },
|
||||
],
|
||||
CALL_ASSIST_DEFAULT,
|
||||
),
|
||||
@@ -305,16 +264,8 @@ export const DEFAULT_AI_PROMPTS: Record<AiActorKey, AiPromptConfig> = {
|
||||
'Analyses post-call recording transcripts to extract key topics, action items, coaching notes, and compliance flags.',
|
||||
[
|
||||
{ key: 'hospitalName', description: 'Branded hospital display name' },
|
||||
{
|
||||
key: 'summaryBlock',
|
||||
description:
|
||||
'Optional pre-rendered "Call summary: ..." line (empty when none)',
|
||||
},
|
||||
{
|
||||
key: 'topicsBlock',
|
||||
description:
|
||||
'Optional pre-rendered "Detected topics: ..." line (empty when none)',
|
||||
},
|
||||
{ key: 'summaryBlock', description: 'Optional pre-rendered "Call summary: ..." line (empty when none)' },
|
||||
{ key: 'topicsBlock', description: 'Optional pre-rendered "Detected topics: ..." line (empty when none)' },
|
||||
],
|
||||
RECORDING_ANALYSIS_DEFAULT,
|
||||
),
|
||||
@@ -329,10 +280,7 @@ export const DEFAULT_AI_CONFIG: AiConfig = {
|
||||
|
||||
// Field-by-field mapping from the legacy env vars used by ai-provider.ts
|
||||
// (AI_PROVIDER + AI_MODEL). API keys are NOT seeded — they remain in env.
|
||||
export const AI_ENV_SEEDS: Array<{
|
||||
env: string;
|
||||
field: keyof Pick<AiConfig, 'provider' | 'model'>;
|
||||
}> = [
|
||||
export const AI_ENV_SEEDS: Array<{ env: string; field: keyof Pick<AiConfig, 'provider' | 'model'> }> = [
|
||||
{ env: 'AI_PROVIDER', field: 'provider' },
|
||||
{ env: 'AI_MODEL', field: 'model' },
|
||||
];
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
LeadNode,
|
||||
LeadActivityNode,
|
||||
CreateCallInput,
|
||||
CreateLeadActivityInput,
|
||||
UpdateLeadInput,
|
||||
} from './platform.types';
|
||||
import type { LeadNode, LeadActivityNode, CreateCallInput, CreateLeadActivityInput, UpdateLeadInput } from './platform.types';
|
||||
|
||||
@Injectable()
|
||||
export class PlatformGraphqlService {
|
||||
@@ -25,18 +19,14 @@ export class PlatformGraphqlService {
|
||||
}
|
||||
|
||||
// Query using a passed-through auth header (user JWT)
|
||||
async queryWithAuth<T>(
|
||||
query: string,
|
||||
variables: Record<string, any> | undefined,
|
||||
authHeader: string,
|
||||
): Promise<T> {
|
||||
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,
|
||||
'Authorization': authHeader,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -73,16 +63,10 @@ export class PlatformGraphqlService {
|
||||
|
||||
// Client-side phone matching (strip non-digits for comparison)
|
||||
const normalizedPhone = phone.replace(/\D/g, '');
|
||||
return (
|
||||
data.leads.edges.find((edge) => {
|
||||
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
|
||||
);
|
||||
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> {
|
||||
@@ -126,9 +110,7 @@ export class PlatformGraphqlService {
|
||||
return data.createCall;
|
||||
}
|
||||
|
||||
async createLeadActivity(
|
||||
input: CreateLeadActivityInput,
|
||||
): Promise<{ id: string }> {
|
||||
async createLeadActivity(input: CreateLeadActivityInput): Promise<{ id: string }> {
|
||||
const data = await this.query<{ createLeadActivity: { id: string } }>(
|
||||
`mutation CreateLeadActivity($data: LeadActivityCreateInput!) {
|
||||
createLeadActivity(data: $data) { id }
|
||||
@@ -140,16 +122,11 @@ export class PlatformGraphqlService {
|
||||
|
||||
// --- Token passthrough versions (for user-driven requests) ---
|
||||
|
||||
async findLeadByPhoneWithToken(
|
||||
phone: string,
|
||||
authHeader: string,
|
||||
): Promise<LeadNode | null> {
|
||||
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 }[] };
|
||||
}>(
|
||||
const data = await this.queryWithAuth<{ leads: { edges: { node: LeadNode }[] } }>(
|
||||
`query FindLeads($first: Int) {
|
||||
leads(first: $first, orderBy: [{ createdAt: DescNullsLast }]) {
|
||||
edges {
|
||||
@@ -171,37 +148,22 @@ export class PlatformGraphqlService {
|
||||
);
|
||||
|
||||
// Client-side phone matching
|
||||
return (
|
||||
data.leads.edges.find((edge) => {
|
||||
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,
|
||||
'',
|
||||
);
|
||||
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, '');
|
||||
const num = ((phones as any).primaryPhoneNumber ?? (phones as any).number ?? '').replace(/\D/g, '');
|
||||
return num.endsWith(last10) || last10.endsWith(num);
|
||||
})?.node ?? null
|
||||
);
|
||||
})?.node ?? null;
|
||||
}
|
||||
|
||||
async getLeadActivitiesWithToken(
|
||||
leadId: string,
|
||||
authHeader: string,
|
||||
limit = 5,
|
||||
): Promise<LeadActivityNode[]> {
|
||||
const data = await this.queryWithAuth<{
|
||||
leadActivities: { edges: { node: LeadActivityNode }[] };
|
||||
}>(
|
||||
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 {
|
||||
@@ -214,14 +176,10 @@ export class PlatformGraphqlService {
|
||||
{ filter: { leadId: { eq: leadId } }, first: limit },
|
||||
authHeader,
|
||||
);
|
||||
return data.leadActivities.edges.map((e) => e.node);
|
||||
return data.leadActivities.edges.map(e => e.node);
|
||||
}
|
||||
|
||||
async updateLeadWithToken(
|
||||
id: string,
|
||||
input: UpdateLeadInput,
|
||||
authHeader: string,
|
||||
): Promise<LeadNode> {
|
||||
async updateLeadWithToken(id: string, input: UpdateLeadInput, authHeader: string): Promise<LeadNode> {
|
||||
// Response fragment deliberately excludes `leadStatus` — the staging
|
||||
// platform schema has this field renamed to `status`. Selecting the
|
||||
// old name rejects the whole mutation. Callers don't use the
|
||||
@@ -249,10 +207,7 @@ export class PlatformGraphqlService {
|
||||
// (`status`, `source`, `lastContacted`) rather than the older
|
||||
// `leadStatus`/`leadSource`/`lastContactedAt` names — otherwise the
|
||||
// query would be rejected on staging.
|
||||
async findLeadByIdWithToken(
|
||||
id: string,
|
||||
authHeader: string,
|
||||
): Promise<any | null> {
|
||||
async findLeadByIdWithToken(id: string, authHeader: string): Promise<any | null> {
|
||||
try {
|
||||
const data = await this.queryWithAuth<{ lead: any }>(
|
||||
`query FindLead($id: UUID!) {
|
||||
@@ -277,9 +232,7 @@ export class PlatformGraphqlService {
|
||||
} catch {
|
||||
// Fall back to edge-style query in case the singular field
|
||||
// doesn't exist on this platform version.
|
||||
const data = await this.queryWithAuth<{
|
||||
leads: { edges: { node: any }[] };
|
||||
}>(
|
||||
const data = await this.queryWithAuth<{ leads: { edges: { node: any }[] } }>(
|
||||
`query FindLead($id: UUID!) {
|
||||
leads(filter: { id: { eq: $id } }, first: 1) {
|
||||
edges {
|
||||
@@ -306,75 +259,10 @@ export class PlatformGraphqlService {
|
||||
}
|
||||
}
|
||||
|
||||
async getPatientWithToken(
|
||||
patientId: string,
|
||||
authHeader: string,
|
||||
): Promise<any | null> {
|
||||
try {
|
||||
const data = await this.queryWithAuth<{ patient: any }>(
|
||||
`query GetPatient($id: UUID!) {
|
||||
patient(filter: { id: { eq: $id } }) {
|
||||
id
|
||||
fullName { firstName lastName }
|
||||
dateOfBirth
|
||||
patientType
|
||||
}
|
||||
}`,
|
||||
{ id: patientId },
|
||||
authHeader,
|
||||
);
|
||||
return data.patient ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getUpcomingAppointmentsWithToken(
|
||||
patientId: string,
|
||||
authHeader: string,
|
||||
limit = 3,
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
const data = await this.queryWithAuth<{
|
||||
appointments: { edges: { node: any }[] };
|
||||
}>(
|
||||
`query GetAppointments($filter: AppointmentFilterInput, $first: Int) {
|
||||
appointments(filter: $filter, first: $first, orderBy: [{ scheduledAt: AscNullsLast }]) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
scheduledAt
|
||||
appointmentType
|
||||
doctorName
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
filter: {
|
||||
patientId: { eq: patientId },
|
||||
scheduledAt: { gte: new Date().toISOString() },
|
||||
},
|
||||
first: limit,
|
||||
},
|
||||
authHeader,
|
||||
);
|
||||
return data.appointments.edges.map((e) => e.node);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 }[] };
|
||||
}>(
|
||||
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 {
|
||||
@@ -386,6 +274,6 @@ export class PlatformGraphqlService {
|
||||
}`,
|
||||
{ filter: { leadId: { eq: leadId } }, first: limit },
|
||||
);
|
||||
return data.leadActivities.edges.map((e) => e.node);
|
||||
return data.leadActivities.edges.map(e => e.node);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user