В обе стороны: синхронизация заявок
Двусторонняя синхронизация — единственная интеграция, которая не сводится к трубопроводу. Эндпоинты простые; работать или не работать будет из-за ответа на вопрос чья запись главная. Ответьте на него сначала на бумаге — и код окажется коротким.
Сначала — владение
Выберите один из трёх вариантов и зафиксируйте его. Четвёртого, который позволяет не выбирать, не существует.
| Схема | Кто что меняет | Когда подходит |
|---|---|---|
| Главная — ваша система | Заявки заводятся и ведутся в вашей системе эксплуатации. У нас — зеркало, наш интерфейс для них преимущественно на чтение. | Диспетчеры продолжают работать там, где привыкли; Tango Vision — карта и витрина данных. |
| Главные — мы | Заявки заводятся и ведутся здесь (с локацией, планом, оборудованием и SLA). Ваша система получает копию для отчётности. | Служба эксплуатации живёт в Building OS, ваша система — архив. |
| Разделение по жизненному циклу | Вы владеете всем до triaged, мы — исполнением с in_progress, вы — закрытием. | Две команды с реальной передачей работы: передача и есть граница владения. |
Какой бы вариант вы ни выбрали, правило одно: по каждому полю ровно одна система имеет право породить изменение. Всё остальное — зеркало, а зеркало никогда не пишет обратно значение, которое только что получило.
Эндпоинты
Отдаются с origin оболочки Building OS, например https://building-os.k8s.tangovision.dev/api/service-desk/…
| Действие | Вызов |
|---|---|
| Создать | POST /api/service-desk/requests |
| Список / опрос | GET /api/service-desk/requests?buildingId={uuid} |
| Прочитать одну | GET /api/service-desk/requests/{id} |
| Изменить поля | PATCH /api/service-desk/requests/{id} |
| Сменить статус | POST /api/service-desk/requests/{id}/transition |
| Назначить | POST /api/service-desk/requests/{id}/assign |
| Добавить запись | POST /api/service-desk/requests/{id}/updates |
Создание принимает buildingId (обязательно), subject, description (обязательно), priority (low medium high critical), category, локацию (spaceId, storeyId, elementId — всё внутренние UUID, см. Идентификаторы), поля заявителя, channel, а также произвольные formData / metadata.
Для заявок, которые создаёт ваша интеграция, ставьте "channel": "system". Это настоящее значение канала, оно видно в интерфейсе и это самый дешёвый способ для оператора отличить зеркальную заявку от заведённой человеком.
Как связать две записи
Поля externalId у заявки нет. Ваш идентификатор кладите в metadata:
{
"buildingId": "…", "description": "Течь в трубе под потолком",
"channel": "system", "spaceId": "…",
"metadata": { "sourceSystem": "acme-fm", "externalId": "WO-2026-4471" }
}Отсюда два следствия:
- Уникальность
metadataничем не обеспечивается. Отправив тот же payload дважды, вы получите две заявки. Держите соответствие у себя — ваш id → нашid— и проверяйте его перед созданием. - Сохраняйте оба возвращённых значения.
id(UUID, нужен всем последующим вызовам) иnumber(человекочитаемый номер, который будут называть пользователи).
Наша машина состояний ограничивает ваше отображение
Переход проверяется, а не принимается на веру:
new → triaged | cancelled
triaged → in_progress | waiting | resolved | cancelled
in_progress → waiting | resolved | cancelled
waiting → in_progress | resolved | cancelled
resolved → closed | reopened
reopened → in_progress | resolved
closed, cancelled → терминальныеТо есть зеркало не может прыгнуть из new сразу в in_progress, и ничто не переоткрывает closed: вернувшаяся проблема — это resolved → reopened или новая заявка. Наложите свои статусы на этот граф до реализации, а если у вас статусов меньше — проводите нашу заявку через промежуточный шаг, а не пытайтесь его пропустить.
Рабочий процесс конкретного типа заявки может сузить эти переходы (добавить он не может), поэтому переход, допустимый для одного типа, для другого может быть отклонён. 400 от /transition — это «здесь так нельзя», а не ошибка.
Две вещи, которые удивляют
Создание заявки у нас может её же и изменить
При создании отрабатывают правила маршрутизации. Заявка, отправленная как new, может вернуться уже triaged, с назначенной командой и поднятым приоритетом, плюс системные записи в ленте. Это платформа работает как настроена, — но если ваша синхронизация трактует «запись у них изменилась» как «вернуть изменение назад», то первая же зеркальная заявка запустит цикл. Игнорируйте изменения, порождённые вашей же записью: сравнивайте с тем, что отправили, либо помечайте запись в metadata и пропускайте эхо.
Часы SLA стартуют в момент создания у нас
slaFirstResponseDueAt и slaResolutionDueAt вычисляются по политике SLA в момент создания в нашей системе, а первое назначение засчитывается как первый ответ. Заявка трёхдневной давности, залитая зеркалом, получит свежие часы, а не исходные, — и отчёт по SLA на зеркале разойдётся с исходной системой. Если заказчику важен SLA, держите его на стороне владельца, а часы другой стороны считайте декоративными.
Наряды
Если используется CAFM, наряды связывает с заявками сама платформа: cafm.work-order.created записывает workOrderId в заявку и добавляет системную запись, .updated добавляет записи о ходе работ, .completed переводит заявку из triaged, in_progress или waiting в resolved (заявитель по-прежнему может её переоткрыть). Обработчики идемпотентны, поэтому повторная доставка не задваивает записи и переходы.
Для внешней интеграции это важно в одном: заявка может сменить статус без участия вашей интеграции. Цикл опроса обязан спокойно переживать статус, который он не вызывал.
Рабочая схема для «главная — ваша система»
- Новая заявка у вас →
POST /requestsсchannel: "system",metadata.externalIdи разрешённымspaceId. Сохраните возвращённыйid. - Изменение полей у вас →
PATCH /requests/{id}; смена статуса →POST /requests/{id}/transitionпо допустимым рёбрам. - Раз в несколько минут → опрашивайте открытые статусы, сравнивайте
updatedAtс тем, что видели, и подтягивайте поля, которыми вы не владеете (назначение, записи, связь с нарядом). - Никогда не пишите обратно то, что пришло на шаге 3.
Шаг 3 — это опрос, потому что события заявок сегодня не доходят до внешних подписчиков, а у списка нет фильтра updatedSince, см. Данные из двойника. И то и другое — известные пробелы, а не замысел; если они вам мешают, скажите: названная заблокированная интеграция — это то, что ставит доработку в план.