@hubex/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.env.example +54 -0
  2. package/CONNECTING.md +260 -0
  3. package/README.md +266 -0
  4. package/dist/auth.js +226 -0
  5. package/dist/config.js +120 -0
  6. package/dist/generated/manifest.js +3643 -0
  7. package/dist/guides/field-notes.json +27 -0
  8. package/dist/guides/loader.js +32 -0
  9. package/dist/guides/service-map.js +6 -0
  10. package/dist/http.js +93 -0
  11. package/dist/index/store.js +134 -0
  12. package/dist/index/types.js +1 -0
  13. package/dist/index.js +20 -0
  14. package/dist/paths.js +20 -0
  15. package/dist/pii/fields.js +100 -0
  16. package/dist/pii/mask.js +50 -0
  17. package/dist/pii/strategies.js +88 -0
  18. package/dist/schema/build-index.js +67 -0
  19. package/dist/schema/deref.js +87 -0
  20. package/dist/schema/describe.js +65 -0
  21. package/dist/server.js +62 -0
  22. package/dist/token-prompt.js +41 -0
  23. package/dist/tools/curated.js +151 -0
  24. package/dist/tools/discovery.js +187 -0
  25. package/dist/tools/guides.js +126 -0
  26. package/dist/tools/registry.js +37 -0
  27. package/dist/tools/request.js +170 -0
  28. package/dist/tools/types.js +1 -0
  29. package/docs/guides/assets.md +334 -0
  30. package/docs/guides/attributes.md +125 -0
  31. package/docs/guides/checklisttemplates.md +154 -0
  32. package/docs/guides/companies.md +57 -0
  33. package/docs/guides/dictionaries.md +64 -0
  34. package/docs/guides/lifecycle.md +263 -0
  35. package/docs/guides/materials.md +135 -0
  36. package/docs/guides/notifications.md +184 -0
  37. package/docs/guides/roles.md +125 -0
  38. package/docs/guides/sla.md +130 -0
  39. package/docs/guides/start.md +67 -0
  40. package/docs/guides/taskchecklists.md +149 -0
  41. package/docs/guides/taskcreate.md +260 -0
  42. package/docs/guides/taskedit.md +277 -0
  43. package/docs/guides/tasktypes.md +156 -0
  44. package/docs/guides/users.md +71 -0
  45. package/generated/index.dev.json +18776 -0
  46. package/generated/index.prod.json +18858 -0
  47. package/package.json +48 -0
  48. package/swagger/dev/ADM.json +27777 -0
  49. package/swagger/dev/AUTH.json +1739 -0
  50. package/swagger/dev/AUTHN.json +1250 -0
  51. package/swagger/dev/AUTHZ.json +1404 -0
  52. package/swagger/dev/CM.json +309 -0
  53. package/swagger/dev/COMMON.json +6543 -0
  54. package/swagger/dev/ES.json +28029 -0
  55. package/swagger/dev/EXPORT.json +4575 -0
  56. package/swagger/dev/IMPORT.json +1479 -0
  57. package/swagger/dev/LIC.json +224 -0
  58. package/swagger/dev/MSG.json +7883 -0
  59. package/swagger/dev/NEWS.json +348 -0
  60. package/swagger/dev/PA.json +5981 -0
  61. package/swagger/dev/PMP.json +3196 -0
  62. package/swagger/dev/PROXY.json +416 -0
  63. package/swagger/dev/REPORT.json +3921 -0
  64. package/swagger/dev/SC.json +3771 -0
  65. package/swagger/dev/SLA.json +2837 -0
  66. package/swagger/dev/TSTG.json +4981 -0
  67. package/swagger/dev/UI.json +4720 -0
  68. package/swagger/dev/WH.json +16796 -0
  69. package/swagger/dev/WORK.json +36024 -0
  70. package/swagger/dev/WSP.json +1612 -0
  71. package/swagger/prod/ADM.json +27777 -0
  72. package/swagger/prod/AUTH.json +1308 -0
  73. package/swagger/prod/AUTHN.json +2710 -0
  74. package/swagger/prod/AUTHZ.json +896 -0
  75. package/swagger/prod/CM.json +162 -0
  76. package/swagger/prod/COMMON.json +4910 -0
  77. package/swagger/prod/ES.json +28029 -0
  78. package/swagger/prod/EXPORT.json +3091 -0
  79. package/swagger/prod/LIC.json +123 -0
  80. package/swagger/prod/MSG.json +6239 -0
  81. package/swagger/prod/NEWS.json +295 -0
  82. package/swagger/prod/PA.json +5123 -0
  83. package/swagger/prod/PMP.json +2978 -0
  84. package/swagger/prod/PROXY.json +250 -0
  85. package/swagger/prod/REPORT.json +3729 -0
  86. package/swagger/prod/SC.json +3771 -0
  87. package/swagger/prod/SLA.json +2201 -0
  88. package/swagger/prod/TSTG.json +4220 -0
  89. package/swagger/prod/UI.json +3879 -0
  90. package/swagger/prod/WH.json +16730 -0
  91. package/swagger/prod/WORK.json +35994 -0
  92. package/swagger/prod/WSP.json +1468 -0
@@ -0,0 +1,260 @@
1
+ ---
2
+ topic: taskcreate
3
+ sources:
4
+ - page: admin/ExampleRequestsAPI.md
5
+ content_hash: ad3e2bf92fe6a736
6
+ - page: admin/TicketLifeCycle.md
7
+ content_hash: 60be5a94c8573779
8
+ ---
9
+
10
+ # Создание заявки
11
+
12
+ Заявка (Task) — центральная сущность HubEx, живёт в сервисе `WORK`. Порядок вызовов
13
+ ниже повторяет то, что делает веб-приложение HubEx при создании заявки, поэтому агент,
14
+ идущий этим путём, получит заявку, неотличимую от созданной руками в интерфейсе.
15
+
16
+ Правка уже существующей заявки, смена стадии, удаление и восстановление — в отдельном
17
+ гайде: `hubex_get_guide topic="taskedit"`.
18
+
19
+ Ключевая особенность: **у `WORK:POST:/Tasks` в swagger нет ни одного обязательного
20
+ поля**, но сервер и бизнес-логика требуют больше, чем схема. Список реально
21
+ обязательных полей отдаёт не swagger, а метаданные формы — `WORK:GET:/Tasks/new/meta`.
22
+ Всегда сверяйся с ними, а не только со схемой.
23
+
24
+ `POST /Tasks` создаёт саму заявку и этого достаточно — она сразу валидна.
25
+ Исполнитель, контакты, доп. поля, файлы и смена стадии — **отдельные запросы
26
+ по полученному `taskID`, и вызываются они только при наличии соответствующих
27
+ данных**, а не всегда (шаг 4).
28
+
29
+ ## Шаг 0. Метаданные и права
30
+
31
+ Для `POST /Tasks` нужно полномочие **TaskAdd** (иначе 403), для метаданных —
32
+ **TaskMetadataGet**.
33
+
34
+ 1. `WORK:GET:/Tasks/new/meta` (опц. query `taskTypeID`) — метаданные формы новой заявки.
35
+ Ответ — словарь по `taskTypeID`; для каждого типа:
36
+ - `components` — поля формы: `{ "<имя поля>": { permission, capability } }`;
37
+ - `attributes` — дополнительные (пользовательские) поля: `{ "<attributeID>": { attribute, attributeType, permission, capability, sortOrder } }`.
38
+
39
+ `capability` определяет доступность и обязательность поля:
40
+ | Код | Смысл |
41
+ | --- | --- |
42
+ | `RO` | только чтение — не передавай |
43
+ | `RW` | можно писать, необязательно |
44
+ | `RWM` | можно писать, **обязательно** (M = mandatory) |
45
+ | `ROM` | обязательно, но заполняется системой |
46
+
47
+ Именно из `capability` берётся обязательность `companyID`, `assetID`, `workTypeID`,
48
+ `payerCompanyID`, `notes`, `contactPerson`, `contactPhone`, `locationID`,
49
+ `estimatedCost`, `attachments` и т.д. — она **разная у разных типов заявок и
50
+ у разных ролей**, зашивать её в код нельзя.
51
+
52
+ 2. `hubex_whoami` — окружение, tenant и от чьего имени идут запросы;
53
+ `ADM:GET:/Users/this/profile` — профиль текущего пользователя, его `userID`
54
+ веб-клиент подставляет в `requestedByUserID`.
55
+
56
+ ### Правило: состав полей выясняй динамически, а не по памяти
57
+
58
+ Набор полей заявки в каждом тенанте свой. Не собирай тело по этому гайду по памяти
59
+ и не придумывай значения за пользователя — действуй так:
60
+
61
+ 1. Возьми `taskTypeID` (если тип не назван — покажи список из `WORK:GET:/TaskTypes`
62
+ и спроси, какой нужен; если тип в тенанте один, бери его молча).
63
+ 2. Запроси `WORK:GET:/Tasks/new/meta` с query `taskTypeID=<выбранный>` **до** сборки тела.
64
+ 3. Разбери ответ на три группы:
65
+ - `capability` = `RWM` в `components` → **обязательное поле**, заявка без него
66
+ не создастся;
67
+ - `capability` = `RWM` в `attributes` → **обязательное доп. поле**, его нужно
68
+ отправить отдельным запросом `WORK:POST:/TaskAttributes` (шаг 4);
69
+ - `capability` = `RW` → опциональное, заполняй, только если пользователь дал данные;
70
+ - `capability` = `RO`/`ROM` → **не передавай**, заполняется системой.
71
+ 4. Сопоставь обязательные поля с тем, что пользователь уже сказал. Значения, которые
72
+ можно вывести однозначно (`requestedByUserID` = текущий пользователь,
73
+ `criticalityID` = запись с `isDefault: true`, `requestMethodID` = 4 для интеграций,
74
+ `companyID` и адрес = из выбранного объекта), подставляй сам и упомяни это в ответе.
75
+ 5. **Одним сообщением спроси только про те обязательные поля, которые остались
76
+ незакрытыми**, и для каждого справочного поля сразу приложи варианты из
77
+ соответствующего справочника (шаг 1). Не задавай вопросы по одному
78
+ и не спрашивай про опциональные поля.
79
+ 6. Только после этого вызывай `WORK:POST:/Tasks`.
80
+
81
+ Тот же цикл повторяется, если пользователь сменил тип заявки: `capability` и состав
82
+ доп. полей у другого типа будут другими — перечитай `Tasks/new/meta` заново.
83
+
84
+ ## Шаг 1. Справочники
85
+
86
+ Все справочники возвращают словарь `{ "<id>": {...} }`, а не массив.
87
+
88
+ | № | Запрос | Что даёт | Полезные query-параметры |
89
+ | --- | --- | --- | --- |
90
+ | 1 | `WORK:GET:/TaskTypes` | `taskTypeID` — тип заявки | `isDeleted=false`, `companyID`, `assetID`, `workTypeID`, `districtID` |
91
+ | 2 | `WORK:GET:/RequestMethods` | `requestMethodID` — способ подачи | `fetch`, `offset` |
92
+ | 3 | `SLA:GET:/Criticalities` | `criticalityID` — критичность | `contractID`, `workTypeID` |
93
+ | 4 | `ES:GET:/Companies` | `companyID` (заказчик), `payerCompanyID`, `payeeCompanyID` | `searchText`; веб шлёт ещё `isDeleted=false`, `isOurCompany=true`, `fetch`, `offset` |
94
+ | 5 | `ES:GET:/Assets` | `assetID` — объект обслуживания | `searchText`, `includePath`; веб дополнительно фильтрует `companyID` и `taskTypeID` |
95
+ | 6 | `WORK:GET:/WorkTypes` | `workTypeID` — вид работ | `isPublished=true`, `assetID`, `taskTypeID` |
96
+ | 7 | `ADM:GET:/Users` (или `ADM:GET:/Users/short`) | `requestedByUserID` — заявитель | `searchText`, `isDeleted`, `isCustomer`, `isTechnician`, `companyID`, `fetch`, `offset` |
97
+ | 8 | `SC:GET:/ServiceContract` | `contractID` — договор | `companyID`, `assetID`, `includeUniversalContractsInAssetFilter=true`, `isDeleted=false` |
98
+ | 9 | `ES:GET:/Assets/{assetID}/contacts`, `ES:GET:/Companies/{companyID}/contacts` | контактные лица для привязки к заявке | — |
99
+
100
+ Параметры `isDeleted`, `companyID`, `taskTypeID`, `isOurCompany` у пунктов 4–5
101
+ отсутствуют в swagger, но реально поддерживаются — веб-клиент шлёт именно их.
102
+ Если фильтр не сработал, отфильтруй ответ на своей стороне.
103
+
104
+ Порядок важен: `assetID` фильтруется по выбранным `companyID` + `taskTypeID`,
105
+ а `workTypeID` — по `assetID` + `taskTypeID`. Выбрал тип заявки → перезапроси
106
+ объекты; выбрал/сменил объект → перезапроси виды работ и договоры, иначе получишь
107
+ несовместимую комбинацию и ошибку валидации на сервере.
108
+
109
+ ## Шаг 2. Адрес (если нужен)
110
+
111
+ В теле заявки адрес передаётся как `locationID`, готового адреса у агента обычно нет.
112
+ Веб-клиент подставляет адрес объекта (`asset.location.id`, при его отсутствии —
113
+ `asset.host.location.id`), а если адрес правили руками — создаёт новый:
114
+
115
+ - `ES:POST:/Locations` — тело **массив**: `[{ address, coordinate, description, isIgnorePossibleDuplication: true }]`.
116
+ Обязателен только `address`. `coordinate` — строка `"LAT:LNG"`. Ответ — массив id,
117
+ `locationID` = первый элемент.
118
+ - `ES:PUT:/Locations` — правка существующего адреса (нужен `id` в теле).
119
+ - Готовые адреса: `ES:GET:/Locations`, `ES:GET:/Locations/{id}`.
120
+
121
+ Проще всего: взять `assetID`, прочитать `ES:GET:/Assets/{assetID}` и использовать
122
+ `location.id` объекта — новый адрес не создавать.
123
+
124
+ ## Шаг 3. Создание заявки
125
+
126
+ `WORK:POST:/Tasks` — тело **один объект** (не массив, в отличие от большинства
127
+ POST-методов HubEx). Опциональный заголовок `X-Concurrency-Stamp` (Guid) даёт
128
+ идемпотентность: повтор с тем же значением вернёт 409.
129
+
130
+ Ответ 201: `{ "id": 12345, "number": "T-12345", "concurrencyStamp": "..." }` —
131
+ `id` и есть `taskID` для всех последующих шагов.
132
+
133
+ ### Обязательный минимум
134
+
135
+ - `taskTypeID` — тип заявки;
136
+ - `requestMethodID` — способ подачи (для интеграций — 4, веб по умолчанию шлёт 1);
137
+ - `criticalityID` — критичность. Берётся из `SLA:GET:/Criticalities`, дефолт —
138
+ запись с `isDefault: true`. Формально в схеме необязательна, но клиент считает
139
+ её обязательной всегда, и заявка без критичности не проходит по SLA.
140
+
141
+ Всё остальное обязательно ровно настолько, насколько это указано в `capability`
142
+ из `Tasks/new/meta` (`RWM`/`ROM`).
143
+
144
+ ### Поля, которые веб-клиент передаёт всегда
145
+
146
+ | Поле | Значение |
147
+ | --- | --- |
148
+ | `taskTypeID` | выбранный тип |
149
+ | `requestMethodID` | способ подачи, по умолчанию 1 |
150
+ | `criticalityID` | критичность по умолчанию |
151
+ | `requestedByUserID` | текущий пользователь (заявитель) |
152
+ | `faultTimestamp` | момент обнаружения неисправности; по умолчанию сегодня, 12:00, формат **без таймзоны** `YYYY-MM-DDTHH:mm:ss` |
153
+ | `estimatedCostCurrencyID` | всегда `1` |
154
+ | `notes` + `notesHtml` | описание в двух видах, см. подводные камни |
155
+
156
+ ### Остальные поля тела
157
+
158
+ `assetID`, `companyID`, `workTypeID`, `locationID`, `parentID` (родительская заявка),
159
+ `contractID`, `assetSchemaID`, `payerCompanyID`, `payeeCompanyID`,
160
+ `contactPerson`, `contactPhone`, `contactEmail`, `number`, `erpID`,
161
+ `requestedStartDateTime`, `requestedFinishDateTime`, `deadline`,
162
+ `estimatedTimeConsumptionMinutes` (целое число минут), `estimatedCost`,
163
+ `serviceLevelAgreementID`, `taskTemplateID`.
164
+
165
+ Форматы дат: `requestedStartDateTime`, `requestedFinishDateTime`, `deadline` —
166
+ ISO 8601 в UTC; `faultTimestamp` — локальное время без таймзоны. `requestedStartDateTime`
167
+ не должен быть позже `requestedFinishDateTime`.
168
+
169
+ Схему с точными типами всегда бери из `hubex_describe_endpoint` для
170
+ `WORK:POST:/Tasks` — она отражает актуальный контракт, а список обязательных
171
+ полей в ней пустой (это не ошибка, см. шаг 0).
172
+
173
+ Быстрый путь для тестовых данных: `hubex_create_test_task` — создаёт заявку с
174
+ разумными дефолтами и префиксом `[MCP-TEST]` в описании, чтобы её потом легко
175
+ было найти и удалить.
176
+
177
+ ## Шаг 4. Досоздание частей заявки (по `taskID`) — по необходимости
178
+
179
+ Всё ниже — **условные шаги**. Каждый вызывается, только если для него есть данные:
180
+ нет исполнителя — не зови `TaskAssignmentHistory`, нет доп. полей — не зови
181
+ `TaskAttributes`, нет файлов — не зови `TaskAttachments`, и т.д. Заявка, созданная
182
+ одним `POST /Tasks`, уже валидна. Порядок ниже — тот, в котором их выполняет
183
+ веб-клиент, если данные есть.
184
+
185
+ Единственное исключение — доп. поля с `capability: RWM` в `attributes` из
186
+ `Tasks/new/meta`: они обязательны, и `WORK:POST:/TaskAttributes` для них нужно
187
+ вызвать сразу после создания заявки, даже если пользователь про них не вспомнил
188
+ (значения у него нужно спросить заранее — см. правило в шаге 0).
189
+
190
+ 1. **Исполнитель** — `WORK:POST:/TaskAssignmentHistory`, тело массив:
191
+ `[{ userID, taskID, scheduledStartDateTime, scheduledFinishDateTime }]`.
192
+ Обязателен `taskID`. `userID: null` — снять назначение. Если задан период,
193
+ должен быть задан и исполнитель, `scheduledStartDateTime` < `scheduledFinishDateTime`.
194
+ Подходящих исполнителей ищи через `ADM:GET:/Users/relevance` с `assetID`
195
+ и `workTypeID` — этот метод ранжирует сотрудников по пригодности к заявке.
196
+ 2. **Дополнительные поля (атрибуты)** — `WORK:POST:/TaskAttributes`, тело массив:
197
+ `[{ taskID, data: [{ attributeID, value, isPublic, sortOrder }] }]`.
198
+ Состав атрибутов и их обязательность — из `attributes` в `Tasks/new/meta`.
199
+ `value` всегда строка: для `SELECT`/`MULTISELECT` — id значений через `|`
200
+ (`"12|15"`), для `ATTACHMENT`/`DRAWING` — id вложений через `|`,
201
+ для `DATE` — `YYYY-MM-DDTHH:mm:ss`.
202
+ 3. **Контакты заявки** — `WORK:POST:/TaskContacts`, тело массив:
203
+ `[{ taskID, data: [contactID, ...] }]`. Удаление — `WORK:DELETE:/TaskContacts`
204
+ с тем же форматом. Список контактов — из контактов объекта/компании (шаг 1, п. 9).
205
+ 4. **Файлы** — `WORK:POST:/TaskAttachments/upload/fromForm` (multipart: `File`,
206
+ `taskID`, `IsPublic`, `IsIgnorePossibleDuplication`, `Description`) или
207
+ `.../upload/fromBody`. Привязать уже загруженное вложение —
208
+ `WORK:POST:/TaskAttachments`.
209
+ 5. **Смена стадии** — только если нужно сразу увести заявку с начальной стадии:
210
+ - доступные переходы: `WORK:GET:/Tasks/{taskID}/stages/next`;
211
+ - обычный переход: `WORK:POST:/TaskStagingHistory`,
212
+ тело объект `{ taskID, taskStageID, criticalityID?, notes? }`;
213
+ - если целевая стадия финальная (`isFinishStage`): вместо этого
214
+ `WORK:PUT:/Tasks/{taskID}/complete` с `{ closed, closedBy, accepted, signed, signedBy }`.
215
+
216
+ У стадии могут быть требования (`TSTG:GET:/TaskStages/{id}/requirements`),
217
+ которые сервер проверит: `AssignedToAnyone` (нужен исполнитель),
218
+ `CheckListsCompleted` (заполнены чек-листы), `SignedByCustomer` (есть подпись).
219
+ Не выполнил — переход отклонят. Подробности — в гайде `taskedit`.
220
+
221
+ После смены стадии права на поля меняются: перечитай `WORK:GET:/Tasks/{taskID}/meta`
222
+ (метаданные уже существующей заявки, в отличие от `Tasks/new/meta`).
223
+
224
+ ## Что дальше
225
+
226
+ Проверить результат: `WORK:GET:/Tasks/{taskID}` (детали), `WORK:GET:/Tasks` (список
227
+ с фильтрами), `WORK:GET:/Tasks/short` (облегчённый список).
228
+
229
+ Любая правка созданной заявки — правка полей, добавление работ и материалов,
230
+ смена стадии, удаление и восстановление — описана в гайде
231
+ `hubex_get_guide topic="taskedit"`. Не используй этот гайд для редактирования:
232
+ там другой метод (`PATCH` со списком `[{ field, value }]`), другие метаданные
233
+ (`WORK:GET:/Tasks/{taskID}/meta`) и логика вычисления дельты.
234
+
235
+ ## Подводные камни
236
+
237
+ - **Обязательность полей динамическая.** Единственный источник правды —
238
+ `capability` из `WORK:GET:/Tasks/new/meta`. Она зависит от типа заявки и роли
239
+ пользователя, поэтому проверять её нужно после выбора `taskTypeID`, а не заранее.
240
+ - **`POST /Tasks` принимает объект, а не массив.** Соседние методы (`/TaskContacts`,
241
+ `/TaskAttributes`, `/TaskAssignmentHistory`, `ES:POST:/Locations`) — наоборот,
242
+ массивы. Перепутать легко, ошибка будет 400.
243
+ - **`notes` и `notesHtml` заполняй оба.** `notes` — текст без разметки, показывается
244
+ в списке заявок; `notesHtml` — с разметкой, показывается в карточке. Веб-клиент
245
+ формирует `notes` как plain-text из `notesHtml`. Заполнишь одно — где-то будет пусто.
246
+ Санитайзер текста/HTML может вернуть 422.
247
+ - **Стадия ≠ статус.** Стадия (`TSTG:GET:/TaskStages`, история —
248
+ `WORK:POST:/TaskStagingHistory`) — крупная фаза жизненного цикла; статус
249
+ (`WORK:GET:/TaskStatuses`) — состояние внутри стадии. Переход, не предусмотренный
250
+ жизненным циклом или не удовлетворяющий требованиям стадии, отклоняется сервером.
251
+ - **Совместимость справочников.** `assetID` должен принадлежать `companyID` и быть
252
+ доступен для `taskTypeID`; `workTypeID` — существовать для этой пары. Не переиспользуй
253
+ списки, полученные до смены типа заявки или объекта.
254
+ - **Ответы коллекций — словари `{ "<id>": {...} }`**, а не массивы; ключ словаря —
255
+ это id, внутри объекта его может не быть.
256
+ - **`faultTimestamp` — без таймзоны**, остальные даты — UTC. Смешаешь форматы —
257
+ получишь сдвиг на часовой пояс, без ошибки от сервера.
258
+ - **Заявка без объекта и компании создастся**, но будет бесполезна: по ней не
259
+ подберётся SLA, договор и исполнитель. Для осмысленного теста указывай
260
+ `companyID` + `assetID` + `workTypeID`.
@@ -0,0 +1,277 @@
1
+ ---
2
+ topic: taskedit
3
+ sources:
4
+ - page: admin/ExampleRequestsAPI.md
5
+ content_hash: ad3e2bf92fe6a736
6
+ - page: admin/TicketLifeCycle.md
7
+ content_hash: 60be5a94c8573779
8
+ ---
9
+
10
+ # Редактирование заявки
11
+
12
+ Этот гайд описывает карточку уже существующей заявки (`Task`) так, как её
13
+ сохраняет MainApp. Основные поля заявки, исполнитель, контакты, доп. поля,
14
+ файлы, материалы, выполненные работы, чек-листы и стадия — независимые части.
15
+ Не отправляй их одним `PATCH /Tasks/{taskID}`: этот метод меняет только поля
16
+ самой карточки.
17
+
18
+ Безопасный порядок: сначала прочитать текущие данные и права, затем рассчитать
19
+ дельту, сохранить связанные сущности, и только последним шагом сменить стадию.
20
+ Закрытая заявка может запретить последующие изменения.
21
+
22
+ ## 1. Прочитай заявку и права на её поля
23
+
24
+ Перед каждой правкой вызови:
25
+
26
+ 1. `WORK:GET:/Tasks/{taskID}` — актуальная карточка;
27
+ 2. `WORK:GET:/Tasks/{taskID}/meta` — права и обязательность полей на текущей
28
+ стадии;
29
+ 3. только для тех частей, которые пользователь хочет менять: `GET` их текущего
30
+ состояния (контакты, атрибуты, файлы, материалы и т. п.).
31
+
32
+ В `meta` поле находится в `components`, дополнительные поля — в `attributes`.
33
+ `capability` имеет тот же смысл, что и при создании:
34
+
35
+ | Код | Действие |
36
+ | --- | --- |
37
+ | `RO` | не изменяй поле |
38
+ | `RW` | можно изменить при явном запросе |
39
+ | `RWM` | поле доступно и обязательно; после изменения не оставляй его пустым |
40
+ | `ROM` | обязательное системное поле, не передавай его вручную |
41
+
42
+ Не используй права, прочитанные до смены стадии: после перехода обязательно
43
+ повтори `WORK:GET:/Tasks/{taskID}/meta`.
44
+
45
+ Для ссылочных значений не придумывай id. Используй те же справочники, что и в
46
+ гайде `hubex_get_guide topic="taskcreate"`: объекты — `ES:GET:/Assets`, виды
47
+ работ — `WORK:GET:/WorkTypes`, пользователей — `ADM:GET:/Users` или
48
+ `ADM:GET:/Users/relevance`, критичности — `SLA:GET:/Criticalities`, договоры —
49
+ `SC:GET:/ServiceContract`.
50
+
51
+ ## 2. Измени основную карточку
52
+
53
+ Основной запрос — `WORK:PATCH:/Tasks/{taskID}`. Его тело — **массив** операций
54
+ `{ field, value }`, а не объект:
55
+
56
+ ```json
57
+ [
58
+ { "field": "notes", "value": "Неисправность подтверждена" },
59
+ { "field": "notesHtml", "value": "<p>Неисправность подтверждена</p>" },
60
+ { "field": "criticalityID", "value": 3 },
61
+ { "field": "deadline", "value": "2026-08-20T09:00:00Z" }
62
+ ]
63
+ ```
64
+
65
+ MainApp при сохранении строит такой список для всех полей формы. Для MCP
66
+ безопаснее передавать только значения, которые пользователь действительно
67
+ изменил: так неизвестное или скрытое поле не будет случайно очищено. Перед
68
+ записью запроси точную схему через `hubex_describe_endpoint` для
69
+ `WORK:PATCH:/Tasks/{taskID}`.
70
+
71
+ Поля, которые MainApp передаёт этим методом:
72
+
73
+ | Поле | Формат / особенность |
74
+ | --- | --- |
75
+ | `companyID`, `assetID`, `workTypeID`, `parentID`, `contractID`, `assetSchemaID` | id из соответствующего справочника |
76
+ | `criticalityID`, `requestedByUserID`, `payerCompanyID`, `payeeCompanyID` | id; не очищай без прямого указания |
77
+ | `contactPerson`, `contactPhone`, `contactEmail`, `erpID`, `number` | строковые поля; телефон передаётся без форматирующих символов |
78
+ | `locationID` | id адреса; создание и правка локации — отдельные вызовы из шага 3 |
79
+ | `requestedStartDateTime`, `requestedFinishDateTime`, `deadline` | ISO 8601 в UTC; начало не позже окончания |
80
+ | `faultTimestamp` | локальное время **без** таймзоны, `YYYY-MM-DDTHH:mm:ss` |
81
+ | `estimatedTimeConsumptionMinutes`, `actualTimeConsumptionMinutes` | целое число минут |
82
+ | `estimatedCost`, `actualCost` | число; валюты MainApp устанавливает в `estimatedCostCurrencyID` и `actualCostCurrencyID` равными `1` |
83
+ | `notes`, `notesHtml` | передавай вместе: `notes` — plain text из `notesHtml` |
84
+
85
+ Чтобы сгенерировать новый номер, MainApp добавляет `{ "field": "number",
86
+ "value": null }`. Не делай этого без явной просьбы пользователя: это не
87
+ очистка номера, а команда серверу сгенерировать другой.
88
+
89
+ ## 3. Адрес и контакты
90
+
91
+ Если меняется адрес, сначала получи или создай `locationID`:
92
+
93
+ - существующий адрес — `ES:GET:/Locations`;
94
+ - новый — `ES:POST:/Locations` с массивом `[{ address, coordinate?,
95
+ isIgnorePossibleDuplication: true }]`;
96
+ - правка существующего — `ES:PUT:/Locations` с массивом, содержащим `id`.
97
+
98
+ После этого передай `locationID` в PATCH заявки. Правка локации влияет на сам
99
+ адрес и может затронуть другие сущности, которые его используют; для нового
100
+ адреса безопаснее создать новую локацию.
101
+
102
+ Контакт из полей `contactPerson`, `contactPhone`, `contactEmail` — это текст в
103
+ карточке. Привязанные контакты объекта или компании — отдельная связь:
104
+
105
+ ```json
106
+ // WORK:POST:/TaskContacts
107
+ [{ "taskID": 12345, "data": [701, 702] }]
108
+
109
+ // WORK:DELETE:/TaskContacts
110
+ [{ "taskID": 12345, "data": [699] }]
111
+ ```
112
+
113
+ Сначала прочитай `WORK:GET:/Tasks/{taskID}/contacts` и вычисли `added`/`removed`.
114
+ Не удаляй все контакты ради замены одного. Кандидаты: `ES:GET:/Assets/{assetID}/contacts`
115
+ и `ES:GET:/Companies/{companyID}/contacts`.
116
+
117
+ ## 4. Исполнители, дополнительные поля и файлы
118
+
119
+ ### Исполнители
120
+
121
+ `WORK:POST:/TaskAssignmentHistory` принимает массив назначений:
122
+
123
+ ```json
124
+ [
125
+ {
126
+ "taskID": 12345,
127
+ "userID": 88,
128
+ "scheduledStartDateTime": "2026-08-14T06:00:00Z",
129
+ "scheduledFinishDateTime": "2026-08-14T14:00:00Z"
130
+ }
131
+ ]
132
+ ```
133
+
134
+ MainApp отправляет новое назначение, когда изменился состав исполнителей или
135
+ период. Для снятия всех назначений он передаёт `[{ taskID, userID: null }]`.
136
+ Если указано расписание, указывай и исполнителя; начало должно быть раньше
137
+ окончания. Подбирать исполнителей следует через `ADM:GET:/Users/relevance` с
138
+ `assetID` и `workTypeID`.
139
+
140
+ ### Дополнительные поля
141
+
142
+ Прочитай `WORK:GET:/Tasks/{taskID}/attributes` и метаданные из шага 1. Меняй
143
+ только отличающиеся от текущих атрибуты через `WORK:POST:/TaskAttributes`:
144
+
145
+ ```json
146
+ [
147
+ {
148
+ "taskID": 12345,
149
+ "data": [
150
+ { "attributeID": 41, "value": "12|15", "isPublic": true, "sortOrder": 0 }
151
+ ]
152
+ }
153
+ ]
154
+ ```
155
+
156
+ `value` — строка: для `SELECT`/`MULTISELECT` это id значений через `|`, для
157
+ `DATE` — `YYYY-MM-DDTHH:mm:ss`, для `ATTACHMENT`/`DRAWING` — id вложений через
158
+ `|`. Новые вложения сначала загрузить, затем передать их id. Не стирай
159
+ обязательный (`RWM`) атрибут.
160
+
161
+ ### Файлы заявки
162
+
163
+ Загрузить новый файл: `WORK:POST:/TaskAttachments/upload/fromForm` как
164
+ `multipart/form-data` с полями `File`, `taskID`, `IsPublic`,
165
+ `IsIgnorePossibleDuplication=true` и, при необходимости, `Description`.
166
+ Связать уже загруженные вложения: `WORK:POST:/TaskAttachments` с телом
167
+ `[{ taskID, data: [attachmentID] }]`. Удалить: `WORK:DELETE:/TaskAttachments`
168
+ с `[{ taskID, data: [attachmentID] }]`.
169
+
170
+ Для сравнения сначала прочитай `WORK:GET:/Tasks/{taskID}/attachments` (MainApp
171
+ добавляет query-параметр `thumbnailSize=128`). Не удаляй вложение только потому,
172
+ что пользователь не упомянул его в новом запросе.
173
+
174
+ ## 5. Материалы и выполненные работы
175
+
176
+ Материалы заявки читай через `WORK:GET:/Tasks/{taskID}/materials`, затем
177
+ выполняй только подходящую операцию:
178
+
179
+ | Операция | Метод |
180
+ | --- | --- |
181
+ | добавить | `WORK:POST:/TaskMaterials` |
182
+ | изменить количество, склад или единицу | `WORK:PUT:/TaskMaterials` |
183
+ | удалить | `WORK:DELETE:/TaskMaterials` |
184
+ | выдать исполнителю | `WORK:PUT:/TaskMaterials/takeOn` |
185
+ | вернуть на склад | `WORK:PUT:/TaskMaterials/takeOff` |
186
+
187
+ Тела первых трёх методов имеют форму `[{ taskID, data: [...] }]`. При создании
188
+ материала MainApp передаёт `materialID`, `warehouseID`, `measurementUnitID`,
189
+ `quantity` и, если материал выдан, `takenByUserID`. Перед вызовом всегда
190
+ прочитай актуальную схему нужного метода: структуры элементов для добавления и
191
+ обновления различаются.
192
+
193
+ Выполненные работы — самостоятельная коллекция, не поле PATCH. Прочитай
194
+ `WORK:GET:/Tasks/{taskID}/completedWorks`, после чего используй
195
+ `WORK:POST:/CompletedWorks`, `WORK:PUT:/CompletedWorks` или
196
+ `WORK:DELETE:/CompletedWorks` с телом `[{ taskID, data: [...] }]`. Основные
197
+ поля работы в MainApp: `maintainedAssetID`, `workTypeID`, `started`, `finished`,
198
+ `notes`, `workTypeCost`.
199
+
200
+ Исполнители, материалы, вложения и атрибуты выполненной работы также имеют
201
+ отдельные методы: `CompletedWorksTechnicians`, `Tasks/completedWorks/materials`,
202
+ `CompletedWorkAttachments/upload/fromForm` и `Tasks/completedWorks/attributes`.
203
+ Используй их только когда пользователь меняет именно эту часть; полные формы
204
+ тел обязательно сверяй через `hubex_describe_endpoint`.
205
+
206
+ ## 6. Чек-листы и стадия
207
+
208
+ Добавление или снятие шаблона чек-листа с заявки выполняется через
209
+ `WORK:POST:/Tasks/{taskID}/checkLists` и
210
+ `WORK:DELETE:/Tasks/{taskID}/checkLists`. MainApp добавляет объекты
211
+ `{ "CheckListID": id }`, а при удалении передаёт массив `taskCheckListID`.
212
+ Заполнение его пунктов описано отдельно в
213
+ `hubex_get_guide topic="taskchecklists"`.
214
+
215
+ Стадия и статус не одно и то же. Стадия — переход жизненного цикла, статус —
216
+ состояние внутри неё. Не пытайся изменить `taskStageID` или `taskStatusID`
217
+ через PATCH.
218
+
219
+ 1. Получи доступные переходы: `WORK:GET:/Tasks/{taskID}/stages/next`.
220
+ 2. Прочитай требования целевой стадии:
221
+ `TSTG:GET:/TaskStages/{id}/requirements`, передав id целевой стадии.
222
+ 3. Для обычной стадии вызови `WORK:POST:/TaskStagingHistory`:
223
+
224
+ ```json
225
+ { "taskID": 12345, "taskStageID": 9 }
226
+ ```
227
+
228
+ 4. Если `isFinishStage: true`, вместо истории стадий заверши заявку через
229
+ `WORK:PUT:/Tasks/{taskID}/complete`. MainApp передаёт как минимум `closed`
230
+ и `closedBy`; дополнительно возможны `accepted`, `acceptedPerson`, `signed`,
231
+ `signedBy`.
232
+
233
+ Типовые требования: `AssignedToAnyone` (назначить исполнителя),
234
+ `CheckListsCompleted` (сохранить все обязательные результаты),
235
+ `SignedByCustomer` (загрузить подпись). Сначала выполни их и проверь результаты,
236
+ затем делай переход. После него перечитай карточку и `meta`.
237
+
238
+ ## 7. Удаление и восстановление
239
+
240
+ Удалить заявку: `WORK:DELETE:/Tasks/{taskID}`. Это отдельное, потенциально
241
+ деструктивное действие — перед вызовом явно подтверди, что пользователь выбрал
242
+ именно эту заявку.
243
+
244
+ Восстановить: `WORK:PUT:/Tasks/restore` с массивом id, например `[12345]`.
245
+ Затем перечитай `WORK:GET:/Tasks/{taskID}`. Не восстанавливай заявку без
246
+ явного запроса пользователя.
247
+
248
+ ## Проверка и порядок операций
249
+
250
+ После каждого изменённого блока перечитывай соответствующий ресурс. При
251
+ комплексном сохранении следуй последовательности MainApp:
252
+
253
+ ```text
254
+ PATCH основной карточки
255
+ → назначение, файлы и контакты
256
+ → результаты чек-листов и выполненные работы
257
+ → дополнительные атрибуты
258
+ → смена стадии
259
+ → GET заявки и GET /meta для проверки
260
+ ```
261
+
262
+ Не запускай зависимые write-запросы параллельно: ошибка в обязательном
263
+ чек-листе, назначении или атрибуте должна быть видна до попытки финального
264
+ перехода.
265
+
266
+ ## Подводные камни
267
+
268
+ - Права и обязательность динамические: источник правды —
269
+ `WORK:GET:/Tasks/{taskID}/meta`, а не swagger и не значения другой заявки.
270
+ - `PATCH /Tasks/{taskID}` принимает массив `{ field, value }`; почти все
271
+ соседние методы принимают объект либо массив объектов с `taskID` и `data`.
272
+ - Не очищай поля значением `null` по умолчанию. `null` в PATCH — это явная
273
+ команда снять значение; для `number` это ещё и запрос на регенерацию номера.
274
+ - `notes` и `notesHtml` должны быть согласованы; `faultTimestamp` не UTC, а
275
+ плановые даты и deadline — UTC.
276
+ - Закрытие и смена стадии выполняются в конце. Если сначала закрыть заявку,
277
+ последующие изменения могут стать недоступны по правам.