From 643219b7006861c6cb0766a18c264e0aa7ee1b5c Mon Sep 17 00:00:00 2001 From: Stanislav Popov Date: Fri, 3 Oct 2025 14:36:30 +0500 Subject: [PATCH 1/2] feat: add text-based sell task creation --- README.md | 22 +- src/server.ts | 2 + ...anfix_create_sell_task.integration.test.ts | 20 +- src/tools/planfix_create_sell_task.test.ts | 174 ++++++------ src/tools/planfix_create_sell_task.ts | 259 ++++++------------ .../planfix_create_sell_task_ids.test.ts | 122 +++++++++ src/tools/planfix_create_sell_task_ids.ts | 200 ++++++++++++++ 7 files changed, 521 insertions(+), 278 deletions(-) create mode 100644 src/tools/planfix_create_sell_task_ids.test.ts create mode 100644 src/tools/planfix_create_sell_task_ids.ts diff --git a/README.md b/README.md index 3750b20..75dda8c 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,23 @@ PLANFIX_TOKEN=your-api-token ### `planfix_create_sell_task` -- Creates a sell task using the Planfix template defined by `PLANFIX_SELL_TEMPLATE_ID`. -- `leadTaskId` is optional; when omitted, the sell task is created without a parent lead task. +- Creates a sell task using textual information about the agency and employee. +- Resolves the client, parent lead task, assignees, and agency IDs automatically based on the provided strings. +- Input fields (all strings): + - `name`: Task title, e.g. `"Продажа {{ название товара }} на pressfinity.com"`. + - `agency`: Agency/company name (optional). + - `email`: Employee email used to locate the Planfix contact. + - `contactName`/`employeeName`: Employee full name (optional). + - `telegram`: Employee telegram username (optional). + - `description`: Description with the list of ordered products. + - `project`: Project name to associate with the sell task (optional). +- Returns `{ taskId, url }`. + +### `planfix_create_sell_task_ids` + +- Creates a sell task when Planfix identifiers are already known. +- Requires numeric `clientId` and optional `leadTaskId`, `agencyId`, and `assignees` (user IDs). +- Accepts `name`, `description`, and optional `project` string values. 5. **Update an object (PUT request)** ```bash @@ -260,7 +275,8 @@ const objects = await planfixClient.post('object/list', { ### Task Management - `searchPlanfixTask`: Search for tasks by title, client ID and optional `templateId` -- `createSellTask`: Create a new sell task with template +- `createSellTask`: Resolve contact/agency IDs and create a sell task +- `createSellTaskIds`: Create a sell task when IDs are already known - `createLeadTask`: Create a new lead task. When `chatApi.useChatApi` is enabled, it sends the initial message through the Chat API, gets the resulting `taskId` via `getTask`, and then updates the task using diff --git a/src/server.ts b/src/server.ts index 3fe367e..a5dd5fc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,6 +11,7 @@ import planfix_create_comment from "./tools/planfix_create_comment.js"; import planfix_create_contact from "./tools/planfix_create_contact.js"; import planfix_create_lead_task from "./tools/planfix_create_lead_task.js"; import planfix_create_sell_task from "./tools/planfix_create_sell_task.js"; +import planfix_create_sell_task_ids from "./tools/planfix_create_sell_task_ids.js"; import planfix_create_task from "./tools/planfix_create_task.js"; import planfix_get_child_tasks from "./tools/planfix_get_child_tasks.js"; import planfix_get_report_fields from "./tools/planfix_get_report_fields.js"; @@ -34,6 +35,7 @@ export const TOOLS: ToolWithHandler[] = [ planfix_create_contact, planfix_create_lead_task, planfix_create_sell_task, + planfix_create_sell_task_ids, planfix_create_task, planfix_get_child_tasks, planfix_get_report_fields, diff --git a/src/tools/planfix_create_sell_task.integration.test.ts b/src/tools/planfix_create_sell_task.integration.test.ts index 50b09b3..7d444cd 100644 --- a/src/tools/planfix_create_sell_task.integration.test.ts +++ b/src/tools/planfix_create_sell_task.integration.test.ts @@ -13,18 +13,16 @@ describe("planfix_create_sell_task tool", () => { "Заказ № 99, Сумма: 690.00, Способ оплаты: Phone ordering, Ссылка на заказ: [ORDER_LINK]Заказ № 99, Сумма: 690.00, Способ оплаты: Phone ordering, Ссылка на заказ: [ORDER_LINK]", }; - const args = { - clientId: taskData.clientId, - leadTaskId: taskData.leadTaskId, - agencyId: taskData.agencyId, - assignees: taskData.assignees, - name: taskData.name, - description: taskData.description, - }; - const { valid, content } = await runTool<{ taskId: number }>( - "planfix_create_sell_task", - args, + "planfix_create_sell_task_ids", + { + clientId: taskData.clientId, + leadTaskId: taskData.leadTaskId, + agencyId: taskData.agencyId, + assignees: taskData.assignees, + name: taskData.name, + description: taskData.description, + }, ); expect(valid).toBe(true); diff --git a/src/tools/planfix_create_sell_task.test.ts b/src/tools/planfix_create_sell_task.test.ts index c094890..ed98aef 100644 --- a/src/tools/planfix_create_sell_task.test.ts +++ b/src/tools/planfix_create_sell_task.test.ts @@ -1,122 +1,110 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; - -vi.mock("../config.js", () => ({ - PLANFIX_DRY_RUN: false, - PLANFIX_FIELD_IDS: { - client: 1, - agency: 2, - leadSource: 3, - serviceMatrix: 4, - }, +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { z } from "zod"; + +vi.mock("./planfix_create_sell_task_ids.js", () => ({ + createSellTaskIds: vi.fn().mockResolvedValue({ taskId: 321, url: "url" }), + CreateSellTaskOutputSchema: z.object({ + taskId: z.number(), + url: z.string(), + }), })); -vi.mock("../helpers.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - planfixRequest: vi.fn().mockResolvedValue({ id: 123 }), - getTaskUrl: (id: number) => `https://example.com/task/${id}`, - log: vi.fn(), - }; -}); - -vi.mock("./planfix_search_project.js", () => ({ - searchProject: vi.fn().mockResolvedValue({ projectId: 10, found: true }), +vi.mock("./planfix_search_lead_task.js", () => ({ + searchLeadTask: vi.fn(), })); -vi.mock("../lib/extendPostBodyWithCustomFields.js", () => ({ - extendPostBodyWithCustomFields: vi.fn(), +vi.mock("./planfix_search_company.js", () => ({ + planfixSearchCompany: vi.fn(), })); -import { planfixRequest } from "../helpers.js"; -import { searchProject } from "./planfix_search_project.js"; import { createSellTask } from "./planfix_create_sell_task.js"; +import { createSellTaskIds } from "./planfix_create_sell_task_ids.js"; +import { searchLeadTask } from "./planfix_search_lead_task.js"; +import { planfixSearchCompany } from "./planfix_search_company.js"; -const mockRequest = vi.mocked(planfixRequest); -const mockSearchProject = vi.mocked(searchProject); +const mockCreateSellTaskIds = vi.mocked(createSellTaskIds); +const mockSearchLeadTask = vi.mocked(searchLeadTask); +const mockSearchCompany = vi.mocked(planfixSearchCompany); describe("createSellTask", () => { - afterEach(() => { + beforeEach(() => { vi.clearAllMocks(); }); - it("sends request with found project", async () => { - process.env.PLANFIX_SELL_TEMPLATE_ID = "11"; - process.env.PLANFIX_FIELD_ID_LEAD_SOURCE_VALUE = "9"; - process.env.PLANFIX_FIELD_ID_SERVICE_MATRIX_VALUE = "8"; + it("resolves identifiers and calls createSellTaskIds", async () => { + mockSearchLeadTask.mockResolvedValue({ + clientId: 42, + taskId: 77, + agencyId: 99, + assignees: { users: [{ id: "user:5" }, { id: "user:8" }] }, + } as any); const result = await createSellTask({ - clientId: 1, - leadTaskId: 2, - agencyId: 3, - assignees: [5], - name: "Name", - description: "Line1\nLine2", - project: "Proj", + name: "Продажа товара", + agency: "Жууу", + email: "agency@example.com", + employeeName: "Имя Сотрудника", + telegram: "agency_telegram", + description: "список товаров", }); - expect(mockSearchProject).toHaveBeenCalledWith({ name: "Proj" }); - const call = mockRequest.mock.calls[0][0]; - const body = call.body as any; - expect(body.project).toEqual({ id: 10 }); - expect(body.assignees.users[0].id).toBe("user:5"); - expect(body.description).toContain("Line1
Line2"); - expect(body.customFieldData).toEqual( - expect.arrayContaining([ - { field: { id: 1 }, value: { id: 1 } }, - { field: { id: 2 }, value: { id: 3 } }, - { field: { id: 3 }, value: { id: 9 } }, - { field: { id: 4 }, value: { id: 8 } }, - ]), - ); - expect(result.taskId).toBe(123); - expect(result.url).toBe("https://example.com/task/123"); - }); - - it("adds project name to description when not found", async () => { - mockSearchProject.mockResolvedValueOnce({ found: false, projectId: 0 }); - - await createSellTask({ - clientId: 1, - leadTaskId: 2, - name: "N", - description: "Desc", - project: "Missing", + expect(mockCreateSellTaskIds).toHaveBeenCalledWith({ + clientId: 42, + leadTaskId: 77, + agencyId: 99, + assignees: [5, 8], + name: "Продажа товара", + description: "список товаров", + project: undefined, }); - - const body = mockRequest.mock.calls[0][0].body as any; - expect(body.description).toContain("Проект: Missing"); + expect(result).toEqual({ taskId: 321, url: "url" }); + expect(mockSearchCompany).not.toHaveBeenCalled(); }); - it("omits parent when leadTaskId is not provided", async () => { - process.env.PLANFIX_SELL_TEMPLATE_ID = "11"; + it("fetches agency id when not provided by search", async () => { + mockSearchLeadTask.mockResolvedValue({ + clientId: 10, + taskId: 0, + assignees: { users: [] }, + } as any); + mockSearchCompany.mockResolvedValue({ contactId: 555 }); await createSellTask({ - clientId: 1, - name: "Name", - description: "Desc", + name: "Продажа", + agency: "Новая", + email: "email@example.com", + contactName: "Имя", + description: "описание", + project: "Proj", }); - const body = mockRequest.mock.calls[0][0].body as any; - expect(body.parent).toBeUndefined(); + expect(mockSearchCompany).toHaveBeenCalledWith({ name: "Новая" }); + expect(mockCreateSellTaskIds).toHaveBeenCalledWith({ + clientId: 10, + leadTaskId: undefined, + agencyId: 555, + assignees: undefined, + name: "Продажа", + description: "описание", + project: "Proj", + }); }); - it("handles dry run", async () => { - const original = await import("../config.js"); - vi.resetModules(); - vi.doMock("../config.js", () => ({ - ...original, - PLANFIX_DRY_RUN: true, - })); - const { createSellTask: createDry } = await import( - "./planfix_create_sell_task.js" + it("throws when contact cannot be resolved", async () => { + mockSearchLeadTask.mockResolvedValue({ + clientId: 0, + taskId: 0, + } as any); + + await expect( + createSellTask({ + name: "Продажа", + email: "missing@example.com", + description: "описание", + }), + ).rejects.toThrow( + "Unable to find a Planfix contact for the provided email/telegram", ); - const res = await createDry({ - clientId: 1, - name: "", - description: "", - }); - expect(res.taskId).toBeGreaterThan(0); - vi.resetModules(); + expect(mockCreateSellTaskIds).not.toHaveBeenCalled(); }); }); diff --git a/src/tools/planfix_create_sell_task.ts b/src/tools/planfix_create_sell_task.ts index 0e9e42b..a40344d 100644 --- a/src/tools/planfix_create_sell_task.ts +++ b/src/tools/planfix_create_sell_task.ts @@ -1,197 +1,114 @@ import { z } from "zod"; -import { PLANFIX_DRY_RUN, PLANFIX_FIELD_IDS } from "../config.js"; +import { getToolWithHandler, log } from "../helpers.js"; import { - getTaskUrl, - getToolWithHandler, - log, - planfixRequest, -} from "../helpers.js"; -import type { CustomFieldDataType } from "../types.js"; -import { searchProject } from "./planfix_search_project.js"; -import { extendSchemaWithCustomFields } from "../lib/extendSchemaWithCustomFields.js"; -import { extendPostBodyWithCustomFields } from "../lib/extendPostBodyWithCustomFields.js"; -import { customFieldsConfig } from "../customFieldsConfig.js"; - -interface TaskRequestBody { - template: { - id: number; - }; - name: string; - description: string; - parent?: { - id: number; - }; - customFieldData: CustomFieldDataType[]; - assignees?: { - users: Array<{ id: string }>; - }; - project?: { - id: number; - }; -} - -const CreateSellTaskInputSchemaBase = z.object({ - clientId: z.number(), - leadTaskId: z.number().optional(), - agencyId: z.number().optional(), - assignees: z.array(z.number()).optional(), - name: z.string(), - description: z.string(), + createSellTaskIds, + CreateSellTaskOutputSchema, +} from "./planfix_create_sell_task_ids.js"; +import { searchLeadTask } from "./planfix_search_lead_task.js"; +import { planfixSearchCompany } from "./planfix_search_company.js"; +import type { UsersListType } from "../types.js"; + +export const CreateSellTaskInputSchema = z.object({ + name: z.string().min(1, "Task name is required"), + agency: z.string().optional(), + email: z.string().min(1, "Email is required"), + contactName: z.string().optional(), + employeeName: z.string().optional(), + telegram: z.string().optional(), + description: z.string().min(1, "Description is required"), project: z.string().optional(), }); -export const CreateSellTaskInputSchema = extendSchemaWithCustomFields( - CreateSellTaskInputSchemaBase, - [], -); +function extractAssigneeIds(assignees?: UsersListType): number[] | undefined { + if (!assignees?.users?.length) { + return undefined; + } -export const CreateSellTaskOutputSchema = z.object({ - taskId: z.number(), - url: z.string(), -}); + const ids = assignees.users + .map((user) => { + if (!user?.id) return undefined; + const match = user.id.match(/(?:user:)?(\d+)/); + if (!match) return undefined; + const parsed = Number(match[1]); + return Number.isFinite(parsed) ? parsed : undefined; + }) + .filter((value): value is number => typeof value === "number"); + + return ids.length ? ids : undefined; +} export async function createSellTask( args: z.infer, ): Promise> { - const { clientId, leadTaskId, agencyId, assignees, project } = args; - let { name, description } = args; + const { + name, + agency, + email, + contactName, + employeeName, + telegram, + description, + project, + } = args; + + const resolvedContactName = contactName ?? employeeName; + + const searchResult = await searchLeadTask({ + name: resolvedContactName, + email, + telegram, + company: agency, + }); + + const { + clientId, + taskId: leadTaskId, + agencyId: initialAgencyId, + assignees, + } = searchResult; + let resolvedAgencyId = initialAgencyId; + + if (!clientId) { + throw new Error( + "Unable to find a Planfix contact for the provided email/telegram", + ); + } - try { - if (PLANFIX_DRY_RUN) { - const mockId = 55500000 + Math.floor(Math.random() * 10000); - const leadTaskLogPart = leadTaskId - ? ` under lead task ${leadTaskId}` - : ""; + if (!resolvedAgencyId && agency) { + try { + const companyResult = await planfixSearchCompany({ name: agency }); + if ("contactId" in companyResult && companyResult.contactId) { + resolvedAgencyId = companyResult.contactId; + } + } catch (error) { log( - `[DRY RUN] Would create sell task for client ${clientId}${leadTaskLogPart}`, + `[createSellTask] Failed to resolve agency '${agency}': ${error instanceof Error ? error.message : String(error)}`, ); - return { taskId: mockId, url: `https://example.com/task/${mockId}` }; - } - - const TEMPLATE_ID = Number(process.env.PLANFIX_SELL_TEMPLATE_ID); - if (!name) name = "Продажа из бота"; - if (!description) description = "Задача продажи для клиента"; - - let finalDescription = description; - let finalProjectId = 0; - - if (project) { - const projectResult = await searchProject({ name: project }); - if (projectResult.found) { - finalProjectId = projectResult.projectId; - } else { - finalDescription = `${finalDescription}\nПроект: ${project}`; - } - } - - finalDescription = finalDescription.replace(/\n/g, "
"); - - const postBody: TaskRequestBody = { - template: { - id: TEMPLATE_ID, - }, - name, - description: finalDescription, - customFieldData: [ - { - field: { - id: PLANFIX_FIELD_IDS.client, - }, - value: { - id: clientId, - }, - }, - ], - }; - - if (typeof leadTaskId === "number") { - postBody.parent = { - id: leadTaskId, - }; } - - if (finalProjectId) { - postBody.project = { id: finalProjectId }; - } - - if (assignees) { - postBody.assignees = { - users: assignees.map((assignee) => ({ - id: `user:${assignee}`, - })), - }; - } - - if (agencyId) { - postBody.customFieldData.push({ - field: { - id: PLANFIX_FIELD_IDS.agency, - }, - value: { - id: agencyId, - }, - }); - } - - const leadSourceValue = Number( - process.env.PLANFIX_FIELD_ID_LEAD_SOURCE_VALUE, - ); - if (leadSourceValue) { - postBody.customFieldData.push({ - field: { - id: PLANFIX_FIELD_IDS.leadSource, - }, - value: { - id: leadSourceValue, - }, - }); - } - - const serviceMatrixValue = Number( - process.env.PLANFIX_FIELD_ID_SERVICE_MATRIX_VALUE, - ); - if (serviceMatrixValue) { - postBody.customFieldData.push({ - field: { - id: PLANFIX_FIELD_IDS.serviceMatrix, - }, - value: { - id: serviceMatrixValue, - }, - }); - } - - await extendPostBodyWithCustomFields( - postBody, - args as Record, - customFieldsConfig.leadTaskFields, - ); - - const result = await planfixRequest<{ id: number }>({ - path: `task/`, - body: postBody as unknown as Record, - }); - const taskId = result.id; - const url = getTaskUrl(taskId); - return { taskId, url }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : "Unknown error"; - log(`[createSellTask] Error: ${errorMessage}`); - throw error; } + + const assigneeIds = extractAssigneeIds(assignees); + + return createSellTaskIds({ + clientId, + leadTaskId: leadTaskId || undefined, + agencyId: resolvedAgencyId, + assignees: assigneeIds, + name, + description, + project, + }); } -async function handler( - args?: Record, -): Promise> { +async function handler(args?: Record) { const parsedArgs = CreateSellTaskInputSchema.parse(args); return createSellTask(parsedArgs); } export const planfixCreateSellTaskTool = getToolWithHandler({ name: "planfix_create_sell_task", - description: "Create a sell task in Planfix", + description: + "Create a sell task in Planfix using textual data for agency and contact", inputSchema: CreateSellTaskInputSchema, outputSchema: CreateSellTaskOutputSchema, handler, diff --git a/src/tools/planfix_create_sell_task_ids.test.ts b/src/tools/planfix_create_sell_task_ids.test.ts new file mode 100644 index 0000000..790c879 --- /dev/null +++ b/src/tools/planfix_create_sell_task_ids.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; + +vi.mock("../config.js", () => ({ + PLANFIX_DRY_RUN: false, + PLANFIX_FIELD_IDS: { + client: 1, + agency: 2, + leadSource: 3, + serviceMatrix: 4, + }, +})); + +vi.mock("../helpers.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + planfixRequest: vi.fn().mockResolvedValue({ id: 123 }), + getTaskUrl: (id: number) => `https://example.com/task/${id}`, + log: vi.fn(), + }; +}); + +vi.mock("./planfix_search_project.js", () => ({ + searchProject: vi.fn().mockResolvedValue({ projectId: 10, found: true }), +})); + +vi.mock("../lib/extendPostBodyWithCustomFields.js", () => ({ + extendPostBodyWithCustomFields: vi.fn(), +})); + +import { planfixRequest } from "../helpers.js"; +import { searchProject } from "./planfix_search_project.js"; +import { createSellTaskIds } from "./planfix_create_sell_task_ids.js"; + +const mockRequest = vi.mocked(planfixRequest); +const mockSearchProject = vi.mocked(searchProject); + +describe("createSellTaskIds", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("sends request with found project", async () => { + process.env.PLANFIX_SELL_TEMPLATE_ID = "11"; + process.env.PLANFIX_FIELD_ID_LEAD_SOURCE_VALUE = "9"; + process.env.PLANFIX_FIELD_ID_SERVICE_MATRIX_VALUE = "8"; + + const result = await createSellTaskIds({ + clientId: 1, + leadTaskId: 2, + agencyId: 3, + assignees: [5], + name: "Name", + description: "Line1\nLine2", + project: "Proj", + }); + + expect(mockSearchProject).toHaveBeenCalledWith({ name: "Proj" }); + const call = mockRequest.mock.calls[0][0]; + const body = call.body as any; + expect(body.project).toEqual({ id: 10 }); + expect(body.assignees.users[0].id).toBe("user:5"); + expect(body.description).toContain("Line1
Line2"); + expect(body.customFieldData).toEqual( + expect.arrayContaining([ + { field: { id: 1 }, value: { id: 1 } }, + { field: { id: 2 }, value: { id: 3 } }, + { field: { id: 3 }, value: { id: 9 } }, + { field: { id: 4 }, value: { id: 8 } }, + ]), + ); + expect(result.taskId).toBe(123); + expect(result.url).toBe("https://example.com/task/123"); + }); + + it("adds project name to description when not found", async () => { + mockSearchProject.mockResolvedValueOnce({ found: false, projectId: 0 }); + + await createSellTaskIds({ + clientId: 1, + leadTaskId: 2, + name: "N", + description: "Desc", + project: "Missing", + }); + + const body = mockRequest.mock.calls[0][0].body as any; + expect(body.description).toContain("Проект: Missing"); + }); + + it("omits parent when leadTaskId is not provided", async () => { + process.env.PLANFIX_SELL_TEMPLATE_ID = "11"; + + await createSellTaskIds({ + clientId: 1, + name: "Name", + description: "Desc", + }); + + const body = mockRequest.mock.calls[0][0].body as any; + expect(body.parent).toBeUndefined(); + }); + + it("handles dry run", async () => { + const original = await import("../config.js"); + vi.resetModules(); + vi.doMock("../config.js", () => ({ + ...original, + PLANFIX_DRY_RUN: true, + })); + const { createSellTaskIds: createDry } = await import( + "./planfix_create_sell_task_ids.js" + ); + const res = await createDry({ + clientId: 1, + name: "", + description: "", + }); + expect(res.taskId).toBeGreaterThan(0); + vi.resetModules(); + }); +}); diff --git a/src/tools/planfix_create_sell_task_ids.ts b/src/tools/planfix_create_sell_task_ids.ts new file mode 100644 index 0000000..1a3d441 --- /dev/null +++ b/src/tools/planfix_create_sell_task_ids.ts @@ -0,0 +1,200 @@ +import { z } from "zod"; +import { PLANFIX_DRY_RUN, PLANFIX_FIELD_IDS } from "../config.js"; +import { + getTaskUrl, + getToolWithHandler, + log, + planfixRequest, +} from "../helpers.js"; +import type { CustomFieldDataType } from "../types.js"; +import { searchProject } from "./planfix_search_project.js"; +import { extendSchemaWithCustomFields } from "../lib/extendSchemaWithCustomFields.js"; +import { extendPostBodyWithCustomFields } from "../lib/extendPostBodyWithCustomFields.js"; +import { customFieldsConfig } from "../customFieldsConfig.js"; + +interface TaskRequestBody { + template: { + id: number; + }; + name: string; + description: string; + parent?: { + id: number; + }; + customFieldData: CustomFieldDataType[]; + assignees?: { + users: Array<{ id: string }>; + }; + project?: { + id: number; + }; +} + +const CreateSellTaskIdsInputSchemaBase = z.object({ + clientId: z.number(), + leadTaskId: z.number().optional(), + agencyId: z.number().optional(), + assignees: z.array(z.number()).optional(), + name: z.string(), + description: z.string(), + project: z.string().optional(), +}); + +export const CreateSellTaskIdsInputSchema = extendSchemaWithCustomFields( + CreateSellTaskIdsInputSchemaBase, + [], +); + +export const CreateSellTaskOutputSchema = z.object({ + taskId: z.number(), + url: z.string(), +}); + +export async function createSellTaskIds( + args: z.infer, +): Promise> { + const { clientId, leadTaskId, agencyId, assignees, project } = args; + let { name, description } = args; + + try { + if (PLANFIX_DRY_RUN) { + const mockId = 55500000 + Math.floor(Math.random() * 10000); + const leadTaskLogPart = leadTaskId + ? ` under lead task ${leadTaskId}` + : ""; + log( + `[DRY RUN] Would create sell task for client ${clientId}${leadTaskLogPart}`, + ); + return { taskId: mockId, url: `https://example.com/task/${mockId}` }; + } + + const TEMPLATE_ID = Number(process.env.PLANFIX_SELL_TEMPLATE_ID); + if (!name) name = "Продажа из бота"; + if (!description) description = "Задача продажи для клиента"; + + let finalDescription = description; + let finalProjectId = 0; + + if (project) { + const projectResult = await searchProject({ name: project }); + if (projectResult.found) { + finalProjectId = projectResult.projectId; + } else { + finalDescription = `${finalDescription}\nПроект: ${project}`; + } + } + + finalDescription = finalDescription.replace(/\n/g, "
"); + + const postBody: TaskRequestBody = { + template: { + id: TEMPLATE_ID, + }, + name, + description: finalDescription, + customFieldData: [ + { + field: { + id: PLANFIX_FIELD_IDS.client, + }, + value: { + id: clientId, + }, + }, + ], + }; + + if (typeof leadTaskId === "number") { + postBody.parent = { + id: leadTaskId, + }; + } + + if (finalProjectId) { + postBody.project = { id: finalProjectId }; + } + + if (assignees) { + postBody.assignees = { + users: assignees.map((assignee) => ({ + id: `user:${assignee}`, + })), + }; + } + + if (agencyId) { + postBody.customFieldData.push({ + field: { + id: PLANFIX_FIELD_IDS.agency, + }, + value: { + id: agencyId, + }, + }); + } + + const leadSourceValue = Number( + process.env.PLANFIX_FIELD_ID_LEAD_SOURCE_VALUE, + ); + if (leadSourceValue) { + postBody.customFieldData.push({ + field: { + id: PLANFIX_FIELD_IDS.leadSource, + }, + value: { + id: leadSourceValue, + }, + }); + } + + const serviceMatrixValue = Number( + process.env.PLANFIX_FIELD_ID_SERVICE_MATRIX_VALUE, + ); + if (serviceMatrixValue) { + postBody.customFieldData.push({ + field: { + id: PLANFIX_FIELD_IDS.serviceMatrix, + }, + value: { + id: serviceMatrixValue, + }, + }); + } + + await extendPostBodyWithCustomFields( + postBody, + args as Record, + customFieldsConfig.leadTaskFields, + ); + + const result = await planfixRequest<{ id: number }>({ + path: `task/`, + body: postBody as unknown as Record, + }); + const taskId = result.id; + const url = getTaskUrl(taskId); + return { taskId, url }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + log(`[createSellTaskIds] Error: ${errorMessage}`); + throw error; + } +} + +async function handler( + args?: Record, +): Promise> { + const parsedArgs = CreateSellTaskIdsInputSchema.parse(args); + return createSellTaskIds(parsedArgs); +} + +export const planfixCreateSellTaskIdsTool = getToolWithHandler({ + name: "planfix_create_sell_task_ids", + description: "Create a sell task in Planfix using numeric identifiers", + inputSchema: CreateSellTaskIdsInputSchema, + outputSchema: CreateSellTaskOutputSchema, + handler, +}); + +export default planfixCreateSellTaskIdsTool; From 94cdcb6e0fd9a137d1fe44b5979eb3804a0e7d7d Mon Sep 17 00:00:00 2001 From: Stanislav Popov Date: Sat, 4 Oct 2025 00:02:37 +0500 Subject: [PATCH 2/2] fix optional clientId --- src/tools/planfix_create_sell_task.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tools/planfix_create_sell_task.ts b/src/tools/planfix_create_sell_task.ts index a40344d..3c32e23 100644 --- a/src/tools/planfix_create_sell_task.ts +++ b/src/tools/planfix_create_sell_task.ts @@ -69,9 +69,7 @@ export async function createSellTask( let resolvedAgencyId = initialAgencyId; if (!clientId) { - throw new Error( - "Unable to find a Planfix contact for the provided email/telegram", - ); + log("Unable to find a Planfix contact for the provided email/telegram"); } if (!resolvedAgencyId && agency) {