Files
crm.clientright.ru/include/Webservices/CreateWebClaimV2.php
Fedor d7982931cd feat: добавлен telegram_replay.php для публикации ответов поддержки в CRM
Новый endpoint для записи ответов поддержки как комментариев в CRM:
- Принимает JSON с полями: answer, contact_id, project_id (опц.), support_user_id (опц.), channel (опц.)
- Использует прямые INSERT запросы в vtiger_crmentity, vtiger_modcomments, vtiger_modcommentscf
- Обязательно создаёт запись в vtiger_modcommentscf (иначе комментарий не отображается)
- Устанавливает deleted=0 (иначе фильтруется при выборке)
- Полная проверка ошибок БД с детальным логированием
- Логи: logs/tg_replay_inbound.log

Исправлены проблемы:
- vtws_create падал без выброса исключения — заменён на прямой SQL
- Убраны несуществующие колонки (from_mailconverter, customer_email, from_mailroom)
- Добавлена обязательная запись в vtiger_modcommentscf
2026-02-03 14:02:12 +03:00

289 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/*********************************************************************************
* API-интерфейс для создания Заявки (HelpDesk) из Web-формы (V2 - JSON версия)
* Принимает JSON строку с данными заявки
* Автор: Фёдор, 2025-12-29
********************************************************************************/
include_once 'include/Webservices/Query.php';
include_once 'modules/Users/Users.php';
require_once('include/Webservices/Utils.php');
require_once 'include/Webservices/Create.php';
require_once 'includes/Loader.php';
vimport ('includes.runtime.Globals');
vimport ('includes.runtime.BaseModel');
vimport ('includes.runtime.LanguageHandler');
/**
* Создание заявки из web-формы ERV Platform (V2 - JSON версия)
*
* @param string $claim_json - JSON строка с данными заявки (обязательно)
* @param object $user - пользователь (опционально)
* @return array - {"ticket_id": "123", "ticket_number": "TT12345", "title": "...", "category": "...", "status": "..."}
*/
function vtws_createwebclaimv2($claim_json, $user = false) {
$logstring = date("Y-m-d H:i:s").' REQUEST: '.json_encode($_REQUEST);
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
// Проверка обязательного параметра
if(empty($claim_json)){
$logstring = date("Y-m-d H:i:s").' Не передан параметр claim_json';
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не передан параметр claim_json");
}
// Парсим JSON
$claimData = json_decode($claim_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// Пробуем очистить от возможных лишних символов
$cleanedJson = trim($claim_json);
$cleanedJson = preg_replace('/^[^{]*/', '', $cleanedJson); // Убираем всё до первой {
$cleanedJson = preg_replace('/[^}]*$/', '', $cleanedJson); // Убираем всё после последней }
$claimData = json_decode($cleanedJson, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$logstring = date("Y-m-d H:i:s").' Ошибка парсинга JSON: '.json_last_error_msg().', JSON: '.substr($claim_json, 0, 200);
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Ошибка парсинга JSON: ".json_last_error_msg());
}
}
$logstring = date("Y-m-d H:i:s").' CLEANED JSON: '.json_encode($claimData);
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
// Извлекаем обязательные поля
$project_id = isset($claimData['project_id']) ? $claimData['project_id'] : '';
$contact_id = isset($claimData['contact_id']) ? $claimData['contact_id'] : '';
$event_type = isset($claimData['cf_1726']) ? $claimData['cf_1726'] : '';
$description = isset($claimData['description']) ? $claimData['description'] : '';
// Проверка обязательных полей
if(empty($project_id)){
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: project_id';
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID проекта");
}
if(empty($contact_id)){
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: contact_id';
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан ID контакта");
}
if(empty($event_type)){
$logstring = date("Y-m-d H:i:s").' Не указано обязательное поле: cf_1726 (event_type)';
file_put_contents('logs/CreateWebClaimV2.log', $logstring.PHP_EOL, FILE_APPEND);
throw new WebServiceException(WebServiceErrorCode::$INVALIDID, "Не указан тип страхового случая");
}
global $adb, $current_user;
// Нормализуем ID контакта и проекта
$contactIdNumeric = preg_replace('/[^0-9]/', '', $contact_id);
$projectIdNumeric = preg_replace('/[^0-9]/', '', $project_id);
$contactWsId = '12x' . $contactIdNumeric;
$projectWsId = '33x' . $projectIdNumeric;
$logstring = date('Y-m-d H:i:s').' Нормализовали ID: contact='.$contactIdNumeric.' (raw='.$contact_id.'), project='.$projectIdNumeric.' (raw='.$project_id.')'.PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
// Маппинг типов событий на русские названия для cf_2650
$eventTypeMap = array(
'delay_flight' => 'Задержка рейса',
'cancel_flight' => 'Отмена рейса',
'miss_connection' => 'Пропуск стыковки',
'missed_connection' => 'Пропуск стыковки',
'delay_train' => 'Задержка поезда',
'cancel_train' => 'Отмена поезда',
'delay_ferry' => 'Задержка парома',
'cancel_ferry' => 'Отмена парома'
);
// ticketcategories всегда "Цифровой адвокат ЕРВ"
$ticketCategory = 'Цифровой адвокат ЕРВ';
// Нормализуем event_type для cf_2650
$normalizedEventType = isset($eventTypeMap[$event_type]) ? $eventTypeMap[$event_type] : 'Цифровой адвокат ЕРВ';
// Извлекаем дополнительные поля
$incident_date = isset($claimData['cf_2566']) ? $claimData['cf_2566'] : '';
$transport_number = isset($claimData['cf_2568']) ? $claimData['cf_2568'] : '';
$cf_1885 = isset($claimData['cf_1885']) ? $claimData['cf_1885'] : '';
$lastname = isset($claimData['lastname']) ? $claimData['lastname'] : '';
$firstname = isset($claimData['firstname']) ? $claimData['firstname'] : '';
// Формируем ticket_title: event_type_cf_1885_lastname_firstname
$ticket_title = $event_type;
if (!empty($cf_1885)) {
$ticket_title .= '_' . $cf_1885;
}
if (!empty($lastname)) {
$ticket_title .= '_' . $lastname;
}
if (!empty($firstname)) {
$ticket_title .= '_' . $firstname;
}
// Формируем описание
$fullDescription = '';
if (!empty($description)) {
$fullDescription .= $description . "\n\n";
}
$fullDescription .= "Тип события: " . $normalizedEventType . "\n";
if (!empty($incident_date)) {
$fullDescription .= "Дата инцидента: " . $incident_date . "\n";
}
if (!empty($transport_number)) {
$fullDescription .= "Номер рейса: " . $transport_number . "\n";
}
// Добавляем cf_departure_flight и cf_departure_date, если есть
$cf_departure_flight = isset($claimData['cf_departure_flight']) ? $claimData['cf_departure_flight'] : '';
$cf_departure_date = isset($claimData['cf_departure_date']) ? $claimData['cf_departure_date'] : '';
if (!empty($cf_departure_flight)) {
$fullDescription .= "Рейс стыковки: " . $cf_departure_flight . "\n";
}
if (!empty($cf_departure_date)) {
$fullDescription .= "Дата стыковки: " . $cf_departure_date . "\n";
}
$fullDescription .= "\nИсточник: ERV Platform Web Form";
// Формируем массив параметров для создания заявки
$params = array(
'ticket_title' => $ticket_title,
'parent_id' => '11x67458', // Заявитель - контрагент
'ticketcategories' => $ticketCategory,
'ticketstatus' => 'рассмотрение',
'contact_id' => $contactWsId,
'cf_2066' => $projectWsId, // Связь с проектом
'ticketpriorities' => 'High',
'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id),
'description' => $fullDescription,
'cf_1726' => $event_type, // Сырой тип события
'cf_2650' => $normalizedEventType // Нормализованный тип события
);
// Маппинг дополнительных полей
if (!empty($incident_date)) {
$params['cf_2566'] = $incident_date;
}
if (!empty($transport_number)) {
$params['cf_2568'] = $transport_number;
}
if (!empty($cf_departure_flight)) {
$params['cf_2630'] = $cf_departure_flight;
}
if (!empty($cf_departure_date)) {
$params['cf_2632'] = $cf_departure_date;
}
// Страна (cf_1909 → cf_2636)
if (isset($claimData['cf_1909']) && !empty($claimData['cf_1909'])) {
$params['cf_2636'] = $claimData['cf_1909'];
}
// cf_2502 → cf_2572
if (isset($claimData['cf_2502']) && !empty($claimData['cf_2502'])) {
$params['cf_2572'] = $claimData['cf_2502'];
}
// code → cf_2574
if (isset($claimData['code']) && !empty($claimData['code'])) {
$params['cf_2574'] = $claimData['code'];
}
// cf_1885 → cf_2642
if (!empty($cf_1885)) {
$params['cf_2642'] = $cf_1885;
}
// IP → cf_2634
if (isset($claimData['ip']) && !empty($claimData['ip'])) {
$params['cf_2634'] = $claimData['ip'];
}
// region → cf_2640
if (isset($claimData['region']) && !empty($claimData['region'])) {
$params['cf_2640'] = $claimData['region'];
}
// source → cf_2638
if (isset($claimData['source']) && !empty($claimData['source'])) {
$params['cf_2638'] = $claimData['source'];
}
// cf_2508 → cf_2508 (прямое маппирование)
if (isset($claimData['cf_2508']) && !empty($claimData['cf_2508'])) {
$params['cf_2508'] = $claimData['cf_2508'];
}
// cf_2648 → cf_2648 (прямое маппирование)
if (isset($claimData['cf_2648']) && !empty($claimData['cf_2648'])) {
$params['cf_2648'] = $claimData['cf_2648'];
}
$logstring = date('Y-m-d H:i:s').' Массив для создания Заявки: '.json_encode($params).PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
try {
$result = vtws_create('HelpDesk', $params, $current_user);
$ticketId = substr($result['id'], 3); // Убираем префикс "17x"
$ticketNumber = isset($result['ticket_no']) ? $result['ticket_no'] : 'N/A';
$logstring = date('Y-m-d H:i:s').' ✅ Создана Заявка id='.$ticketId.' ticket_no='.$ticketNumber.PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
// Создаём двустороннюю связь между Проектом и Заявкой
try {
$relationCheck = $adb->pquery(
"SELECT 1 FROM vtiger_crmentityrel
WHERE (crmid = ? AND relcrmid = ?)
OR (crmid = ? AND relcrmid = ?)
LIMIT 1",
array($projectIdNumeric, $ticketId, $ticketId, $projectIdNumeric)
);
if (!$relationCheck || $adb->num_rows($relationCheck) === 0) {
$adb->pquery(
"INSERT INTO vtiger_crmentityrel (crmid, module, relcrmid, relmodule) VALUES (?, ?, ?, ?)",
array($projectIdNumeric, 'Project', $ticketId, 'HelpDesk')
);
$logstring = date('Y-m-d H:i:s').' 🔗 Добавлена связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.')'.PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
} else {
$logstring = date('Y-m-d H:i:s').' 🔗 Связь Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.') уже существует'.PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
}
} catch (Exception $relEx) {
$logstring = date('Y-m-d H:i:s').' ⚠️ Ошибка связывания Project('.$projectIdNumeric.') ⇄ HelpDesk('.$ticketId.'): '.$relEx->getMessage().PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
}
// Возвращаем массив
$output = array(
'ticket_id' => $ticketId,
'ticket_number' => $ticketNumber,
'title' => $ticket_title,
'category' => $ticketCategory,
'status' => 'рассмотрение'
);
} catch (WebServiceException $ex) {
$logstring = date('Y-m-d H:i:s').' ❌ Ошибка создания: '.$ex->getMessage().PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
throw $ex;
}
$logstring = date('Y-m-d H:i:s').' Return: '.json_encode($output).PHP_EOL;
file_put_contents('logs/CreateWebClaimV2.log', $logstring, FILE_APPEND);
return $output;
}