• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

popstas / planfix-mcp-server / 18100377366

29 Sep 2025 02:26PM UTC coverage: 80.281%. First build
18100377366

Pull #52

github

web-flow
Merge 87c0faebb into d2745b39e
Pull Request #52: Handle Planfix chat API response variations

450 of 630 branches covered (71.43%)

Branch coverage included in aggregate %.

14 of 36 new or added lines in 2 files covered. (38.89%)

3153 of 3858 relevant lines covered (81.73%)

2.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.25
/src/tools/planfix_create_lead_task.ts
1
import { z } from "zod";
1✔
2
import { PLANFIX_DRY_RUN, PLANFIX_FIELD_IDS } from "../config.js";
1✔
3
import {
1✔
4
  getTaskUrl,
5
  getToolWithHandler,
6
  log,
7
  planfixRequest,
8
} from "../helpers.js";
9
import { searchProject } from "./planfix_search_project.js";
1✔
10
import {
1✔
11
  addDirectoryEntry,
12
  addDirectoryEntries,
13
} from "../lib/planfixDirectory.js";
14
import { getTaskCustomFieldName } from "../lib/planfixCustomFields.js";
1✔
15
import { TaskRequestBody } from "../types.js";
16
import { customFieldsConfig, chatApiConfig } from "../customFieldsConfig.js";
1✔
17
import { extendSchemaWithCustomFields } from "../lib/extendSchemaWithCustomFields.js";
1✔
18
import { extendPostBodyWithCustomFields } from "../lib/extendPostBodyWithCustomFields.js";
1✔
19
import {
1✔
20
  chatApiRequest,
21
  ChatApiChatResponse,
22
  ChatApiNumberResponse,
23
  getChatId,
24
} from "../chatApi.js";
25
import { updateLeadTask } from "./planfix_update_lead_task.js";
1✔
26
import { updatePlanfixContact } from "./planfix_update_contact.js";
1✔
27

28
const CreateLeadTaskInputSchemaBase = z.object({
1✔
29
  name: z.string().optional().describe("Name of the task"),
1✔
30
  description: z.string(),
1✔
31
  clientId: z.number(),
1✔
32
  managerId: z.number().optional(),
1✔
33
  agencyId: z.number().optional(),
1✔
34
  project: z.string().optional(),
1✔
35
  leadSource: z.string().optional(),
1✔
36
  pipeline: z.string().optional(),
1✔
37
  tags: z.array(z.string()).optional(),
1✔
38
  leadId: z.number().optional(),
1✔
39
  message: z.string().optional().describe("Initial message text for chat"),
1✔
40
  contactName: z.string().optional().describe("Name of the contact"),
1✔
41
  email: z.string().optional(),
1✔
42
  phone: z.string().optional(),
1✔
43
  telegram: z.string().optional(),
1✔
44
  instagram: z.string().optional(),
1✔
45
});
1✔
46

47
export const CreateLeadTaskInputSchema = extendSchemaWithCustomFields(
1✔
48
  CreateLeadTaskInputSchemaBase,
1✔
49
  customFieldsConfig.leadTaskFields,
1✔
50
);
1✔
51

52
export const CreateLeadTaskOutputSchema = z.object({
1✔
53
  taskId: z.number(),
1✔
54
  url: z.string().optional(),
1✔
55
  error: z.string().optional(),
1✔
56
});
1✔
57

58
/**
59
 * Create a new lead task in Planfix
60
 * @param name - The name of the task
61
 * @param description - The description of the task
62
 * @param clientId - The ID of the client
63
 * @param managerId - Optional ID of the manager
64
 * @param agencyId - Optional ID of the agency
65
 * @param project - Optional name of the project
66
 * @param leadSource - Optional name of the lead source
67
 * @param tags - Optional array of tags
68
 * @returns Promise with the created task ID and URL
69
 */
70
export async function createLeadTask(
3✔
71
  args: z.infer<typeof CreateLeadTaskInputSchema>,
3✔
72
): Promise<{
73
  taskId: number;
74
  url?: string;
75
  error?: string;
76
}> {
3✔
77
  const {
3✔
78
    name,
3✔
79
    description,
3✔
80
    clientId,
3✔
81
    managerId,
3✔
82
    agencyId,
3✔
83
    project,
3✔
84
    leadSource,
3✔
85
    pipeline,
3✔
86
    leadId,
3✔
87
    tags,
3✔
88
    message,
3✔
89
  } = args;
3✔
90
  if (chatApiConfig.useChatApi) {
3✔
91
    const chatId = getChatId(args);
1✔
92
    const chatParams = {
1✔
93
      chatId,
1✔
94
      contactId: clientId,
1✔
95
      title: name,
1✔
96
      message: description || message,
1!
97
    };
1✔
98
    try {
1✔
99
      await chatApiRequest<ChatApiChatResponse>("newMessage", chatParams);
1✔
100
      const data = await chatApiRequest<
1✔
101
        ChatApiNumberResponse & { data?: { number?: number } }
102
      >("getTask", {
1✔
103
        chatId,
1✔
104
      });
1✔
105
      const taskId =
1✔
106
        typeof data.number === "number"
1!
NEW
107
          ? data.number
×
108
          : typeof data.data?.number === "number"
1✔
109
            ? data.data.number
1!
NEW
110
            : undefined;
×
111
      if (typeof taskId !== "number") {
1!
NEW
112
        throw new Error("Chat API response does not contain task number");
×
NEW
113
      }
×
114
      await updateLeadTask({ ...(args as Record<string, unknown>), taskId });
1✔
115
      const contactArgs: Record<string, unknown> = {
1✔
116
        ...(args as Record<string, unknown>),
1✔
117
        contactId: clientId,
1✔
118
        name,
1✔
119
      };
1✔
120
      delete contactArgs.name;
1✔
121
      delete contactArgs.message;
1✔
122
      await updatePlanfixContact(
1✔
123
        contactArgs as Parameters<typeof updatePlanfixContact>[0],
1✔
124
      );
1✔
125
      return { taskId, url: getTaskUrl(taskId) };
1✔
126
    } catch (error) {
1!
127
      const errorMessage =
×
128
        error instanceof Error ? error.message : "Unknown error";
×
129
      return { taskId: 0, error: errorMessage };
×
130
    }
×
131
  }
1✔
132
  const TEMPLATE_ID = Number(process.env.PLANFIX_LEAD_TEMPLATE_ID);
2✔
133
  let finalDescription = description;
2✔
134
  let finalProjectId = 0;
2✔
135

136
  if (project) {
3✔
137
    const projectResult = await searchProject({ name: project });
1✔
138
    if (projectResult.found) {
1✔
139
      finalProjectId = projectResult.projectId;
1✔
140
    } else {
1!
141
      finalDescription = `${finalDescription}\nПроект: ${project}`;
×
142
    }
×
143
  }
1✔
144

145
  finalDescription = finalDescription.replace(/\n/g, "<br>");
2✔
146

147
  const postBody: TaskRequestBody = {
2✔
148
    template: {
2✔
149
      id: TEMPLATE_ID,
2✔
150
    },
2✔
151
    name,
2✔
152
    description: finalDescription,
2✔
153
    customFieldData: [
2✔
154
      {
2✔
155
        field: {
2✔
156
          id: PLANFIX_FIELD_IDS.client,
2✔
157
        },
2✔
158
        value: {
2✔
159
          id: clientId,
2✔
160
        },
2✔
161
      },
2✔
162
    ],
2✔
163
  };
2✔
164

165
  if (finalProjectId) {
3✔
166
    postBody.project = { id: finalProjectId };
1✔
167
  }
1✔
168

169
  if (managerId) {
3✔
170
    if (PLANFIX_FIELD_IDS.manager) {
1✔
171
      postBody.customFieldData.push({
1✔
172
        field: { id: PLANFIX_FIELD_IDS.manager },
1✔
173
        value: { id: managerId },
1✔
174
      });
1✔
175
    } else {
1!
176
      postBody.assignees = { users: [{ id: `user:${managerId}` }] };
×
177
    }
×
178
  }
1✔
179

180
  if (leadSource) {
3✔
181
    await addDirectoryEntry({
1✔
182
      objectId: TEMPLATE_ID,
1✔
183
      fieldId: PLANFIX_FIELD_IDS.leadSource,
1✔
184
      value: leadSource,
1✔
185
      postBody,
1✔
186
    });
1✔
187
  } else {
1✔
188
    const leadSourceValue = Number(
1✔
189
      process.env.PLANFIX_FIELD_ID_LEAD_SOURCE_VALUE,
1✔
190
    );
1✔
191
    if (leadSourceValue) {
1!
192
      postBody.customFieldData.push({
×
193
        field: { id: PLANFIX_FIELD_IDS.leadSource },
×
194
        value: { id: leadSourceValue },
×
195
      });
×
196
    }
×
197
  }
1✔
198

199
  if (pipeline) {
3✔
200
    await addDirectoryEntry({
1✔
201
      objectId: TEMPLATE_ID,
1✔
202
      fieldId: PLANFIX_FIELD_IDS.pipeline,
1✔
203
      value: pipeline,
1✔
204
      postBody,
1✔
205
    });
1✔
206
  }
1✔
207

208
  if (leadId && PLANFIX_FIELD_IDS.leadId) {
3!
209
    postBody.customFieldData.push({
×
210
      field: { id: PLANFIX_FIELD_IDS.leadId },
×
211
      value: leadId,
×
212
    });
×
213
  }
✔
214

215
  if (agencyId) {
3✔
216
    postBody.customFieldData.push({
1✔
217
      field: { id: PLANFIX_FIELD_IDS.agency },
1✔
218
      value: { id: agencyId },
1✔
219
    });
1✔
220
  }
1✔
221

222
  if (tags?.length && PLANFIX_FIELD_IDS.tags && !PLANFIX_DRY_RUN) {
3✔
223
    await addDirectoryEntries({
1✔
224
      objectId: TEMPLATE_ID,
1✔
225
      fieldId: PLANFIX_FIELD_IDS.tags,
1✔
226
      values: tags,
1✔
227
      postBody,
1✔
228
    });
1✔
229
  }
1✔
230

231
  await extendPostBodyWithCustomFields(
2✔
232
    postBody,
2✔
233
    args as Record<string, unknown>,
2✔
234
    customFieldsConfig.leadTaskFields,
2✔
235
  );
2✔
236

237
  try {
2✔
238
    if (PLANFIX_DRY_RUN) {
3!
239
      const mockId = 55500000 + Math.floor(Math.random() * 10000);
×
240
      log(`[DRY RUN] Would create lead task: ${name}`);
×
241
      return { taskId: mockId, url: `https://example.com/task/${mockId}` };
×
242
    }
✔
243

244
    const result = await planfixRequest<{ id: number }>({
2✔
245
      path: `task/`,
2✔
246
      body: postBody as unknown as Record<string, unknown>,
2✔
247
    });
2✔
248
    const taskId = result.id;
1✔
249
    const url = getTaskUrl(taskId);
1✔
250

251
    return { taskId, url };
1✔
252
  } catch (error) {
1✔
253
    let errorMessage = error instanceof Error ? error.message : "Unknown error";
1!
254
    const match = /custom_field_is_required, id (\d+)/i.exec(errorMessage);
1✔
255
    if (match) {
1✔
256
      try {
1✔
257
        const fieldId = Number(match[1]);
1✔
258
        const fieldName = await getTaskCustomFieldName(fieldId);
1✔
259
        if (fieldName) {
1✔
260
          errorMessage += `, name: ${fieldName}`;
1✔
261
        }
1✔
262
      } catch (e) {
1!
263
        log(
×
264
          `[createLeadTask] Failed to get field name: ${(e as Error).message}`,
×
265
        );
×
266
      }
×
267
    }
1✔
268
    log(`[createLeadTask] Error: ${errorMessage}`);
1✔
269
    const requestStr = JSON.stringify(postBody);
1✔
270
    return {
1✔
271
      taskId: 0,
1✔
272
      error: `Error creating task: ${errorMessage}, request: ${requestStr}`,
1✔
273
    };
1✔
274
  }
1✔
275
}
3✔
276

277
export async function handler(
×
278
  args?: Record<string, unknown>,
×
279
): Promise<z.infer<typeof CreateLeadTaskOutputSchema>> {
×
280
  const parsedArgs = CreateLeadTaskInputSchema.parse(args);
×
281
  return await createLeadTask(parsedArgs);
×
282
}
×
283

284
export const planfixCreateLeadTaskTool = getToolWithHandler({
1✔
285
  name: "planfix_create_lead_task",
1✔
286
  description: "Create a new lead task in Planfix",
1✔
287
  inputSchema: CreateLeadTaskInputSchema,
1✔
288
  outputSchema: CreateLeadTaskOutputSchema,
1✔
289
  handler,
1✔
290
});
1✔
291

292
export default planfixCreateLeadTaskTool;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc