События телефонии приходят не в том порядке, в каком случились
В карточке звонок «завершён», потом снова «идёт». Длительность отрицательная.
restочереди
Интеграция принимает вебхуки о звонке: начался, ответили, завершился. В карточке CRM звонок сначала помечается завершённым, а через секунду снова становится активным. Иногда длительность выходит отрицательной.
Причина
Провайдер отправляет события параллельно, и порядок доставки не гарантирован. Событие «завершён» уходит по более быстрому маршруту и приходит раньше, чем «ответили». Ваш обработчик применяет их в порядке получения — и состояние затирается более старым.
Это не сбой провайдера. Так устроена любая доставка через сеть без явной синхронизации: гарантируется факт доставки, но не очерёдность.
Обычно в теле события есть поле последовательности — счётчик или точная метка времени на стороне провайдера. Именно оно описывает реальный порядок, а не момент, когда запрос долетел до вас.
Решение
Хранить у звонка номер последнего применённого события и отбрасывать всё, что старше:
UPDATE calls
SET status = :status,
last_seq = :seq
WHERE call_id = :call_id
AND last_seq < :seq;
Ноль обновлённых строк — событие устаревшее, применять нечего. Это одновременно решает и вторую проблему: повторную доставку. Провайдеры переотправляют вебхуки при таймауте, и без такой проверки один звонок обработается дважды.
Условие должно быть в самом запросе, а не в коде до него. Проверка «сначала прочитали, потом сравнили, потом записали» разваливается, когда два вебхука обрабатываются одновременно: оба прочитают старое значение и оба решат, что имеют право писать.
Что ещё стоит учесть
Событие может прийти раньше, чем создан объект. Если «ответили» обогнало «начался», записи о звонке ещё нет. Обработчик должен уметь создать её на месте, а не падать с «звонок не найден».
Метки времени берём провайдерские. Своё время постановки в очередь для восстановления порядка не годится: оно как раз и отражает кривой порядок доставки.
Как проверить, что чинилось не зря
Прогнать события звонка в обратном порядке. Итоговое состояние карточки должно совпасть с прямым прогоном — если совпадает, интеграция больше не зависит от прихотей маршрутизации.