Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
20 changes: 9 additions & 11 deletions src/tools/planfix_create_sell_task.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
174 changes: 81 additions & 93 deletions src/tools/planfix_create_sell_task.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("../helpers.js")>();
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<br>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(

Check failure on line 105 in src/tools/planfix_create_sell_task.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tools/planfix_create_sell_task.test.ts > createSellTask > throws when contact cannot be resolved

AssertionError: promise resolved "{ taskId: 321, url: 'url' }" instead of rejecting - Expected + Received - Error { - "message": "rejected promise", + { + "taskId": 321, + "url": "url", } ❯ src/tools/planfix_create_sell_task.test.ts:105:5
"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();
});
});
Loading
Loading