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
This commit is contained in:
Fedor
2026-02-03 14:02:12 +03:00
parent ea0edafba5
commit d7982931cd
2 changed files with 474 additions and 0 deletions

186
telegram_replay.php Normal file
View File

@@ -0,0 +1,186 @@
<?php
/*********************************************************************************
* telegram_replay.php — публикация в CRM ответов поддержки на вопросы пользователя
*
* Вход (POST JSON):
* answer — текст ответа поддержки (обязательно)
* contact_id — ID контакта в CRM, кому отвечаем (обязательно)
* project_id — ID проекта (необязательно; для ответственного и уведомлений)
* support_user_id — ID сотрудника CRM, кто ответил (необязательно; иначе админ/ответственный по проекту)
* channel — канал, например 'Support' или 'Telegram' (необязательно, по умолчанию 'Support')
*
* Логи: logs/tg_replay_inbound.log
********************************************************************************/
error_reporting(E_ALL);
ini_set('display_errors', '1');
include_once 'modules/Users/Users.php';
include_once 'include/utils/CommonUtils.php';
include_once 'include/utils/utils.php';
require_once 'include/Webservices/Utils.php';
require_once 'include/Webservices/Create.php';
require_once 'include/Webservices/Revise.php';
require_once 'include/utils/WhatsApp.php';
require_once 'includes/Loader.php';
vimport('includes.runtime.Globals');
vimport('includes.runtime.BaseModel');
vimport('includes.runtime.LanguageHandler');
$logFile = 'logs/tg_replay_inbound.log';
$str = file_get_contents('php://input');
$logstring = date('Y-m-d H:i:s') . ' ' . $str . PHP_EOL;
file_put_contents($logFile, $logstring, FILE_APPEND);
$data = json_decode($str, true);
if (!is_array($data)) {
file_put_contents($logFile, date('Y-m-d H:i:s') . ' Ошибка: невалидный JSON' . PHP_EOL, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Invalid JSON']);
exit;
}
$answer = isset($data['answer']) ? trim($data['answer']) : (isset($data['message']) ? trim($data['message']) : '');
$contact_id = isset($data['contact_id']) ? (int) $data['contact_id'] : 0;
$project_id = isset($data['project_id']) ? (int) $data['project_id'] : 0;
$support_user_id = isset($data['support_user_id']) ? $data['support_user_id'] : null; // может быть "19x123"
$channel = isset($data['channel']) ? trim($data['channel']) : 'Support';
if (empty($answer)) {
file_put_contents($logFile, date('Y-m-d H:i:s') . ' Ошибка: не передан answer/message' . PHP_EOL, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Missing answer or message']);
exit;
}
if ($contact_id <= 0) {
file_put_contents($logFile, date('Y-m-d H:i:s') . ' Ошибка: не передан или неверный contact_id' . PHP_EOL, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Missing or invalid contact_id']);
exit;
}
global $adb;
if (empty($adb)) {
$adb = PearDatabase::getInstance();
}
file_put_contents($logFile, date('Y-m-d H:i:s') . ' $adb ok' . PHP_EOL, FILE_APPEND);
// Проверяем, что контакт существует
$check = $adb->pquery(
'SELECT c.contactid, e.smownerid FROM vtiger_contactdetails c
INNER JOIN vtiger_crmentity e ON e.crmid = c.contactid
WHERE e.deleted = 0 AND c.contactid = ?',
array($contact_id)
);
if ($adb->num_rows($check) === 0) {
file_put_contents($logFile, date('Y-m-d H:i:s') . ' Ошибка: контакт с ID ' . $contact_id . ' не найден' . PHP_EOL, FILE_APPEND);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['success' => false, 'error' => 'Contact not found']);
exit;
}
file_put_contents($logFile, date('Y-m-d H:i:s') . ' Контакт ' . $contact_id . ' найден' . PHP_EOL, FILE_APPEND);
$owner_id = $adb->query_result($check, 0, 'smownerid');
$crmid = '12x' . $contact_id;
$setype = 'Contacts';
// Кто создаёт комментарий: переданный support_user_id, иначе ответственный по проекту, иначе по контакту, иначе админ
$user = null;
if (!empty($support_user_id)) {
$user = (strpos($support_user_id, 'x') !== false) ? $support_user_id : ('19x' . $support_user_id);
} elseif ($project_id > 0) {
$proj = $adb->pquery(
'SELECT e.smownerid FROM vtiger_project p INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
WHERE e.deleted = 0 AND p.projectid = ? AND p.linktoaccountscontacts = ?',
array($project_id, $contact_id)
);
if ($adb->num_rows($proj) > 0) {
$uid = $adb->query_result($proj, 0, 'smownerid');
$user = '19x' . $uid;
}
}
if (empty($user)) {
$user = '19x' . $owner_id;
}
if (empty($user) || $user === '19x') {
$user = Users::getActiveAdminUser();
}
$owner_id_num = (strpos($user, 'x') !== false) ? (int) substr($user, strpos($user, 'x') + 1) : (int) $user;
file_put_contents($logFile, date('Y-m-d H:i:s') . ' user=' . (string)$user . ' (owner_id_num=' . $owner_id_num . ')' . PHP_EOL, FILE_APPEND);
// Запись комментария напрямую в БД (без vtws_create — тот падает внутри без выброса исключения)
$commentCrmId = $adb->getUniqueID('vtiger_crmentity');
$date_var = date('Y-m-d H:i:s');
// deleted=0 обязательно — иначе запись не попадёт в выборку (WHERE vtiger_crmentity.deleted = 0)
$sql_crmentity = "INSERT INTO vtiger_crmentity (crmid, smcreatorid, smownerid, smgroupid, setype, description, modifiedby, createdtime, modifiedtime, source, deleted) VALUES (?, ?, ?, 0, 'ModComments', '', ?, ?, ?, 'CRM', 0)";
$result1 = $adb->pquery($sql_crmentity, array($commentCrmId, $owner_id_num, $owner_id_num, $owner_id_num, $date_var, $date_var));
if (!$result1) {
$error = $adb->database->ErrorMsg();
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ❌ Ошибка INSERT vtiger_crmentity: ' . $error . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'error' => 'DB error: crmentity - ' . $error]);
exit;
}
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ✅ vtiger_crmentity OK' . PHP_EOL, FILE_APPEND);
$sql_modcomments = "INSERT INTO vtiger_modcomments (modcommentsid, commentcontent, related_to, parent_comments, customer, userid, reasontoedit, is_private, filename, related_email_id, channel, messageid) VALUES (?, ?, ?, '', 0, 0, NULL, 0, NULL, NULL, ?, NULL)";
$result2 = $adb->pquery($sql_modcomments, array($commentCrmId, $answer, $contact_id, $channel));
if (!$result2) {
$error = $adb->database->ErrorMsg();
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ❌ Ошибка INSERT vtiger_modcomments: ' . $error . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'error' => 'DB error: modcomments - ' . $error]);
exit;
}
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ✅ vtiger_modcomments OK' . PHP_EOL, FILE_APPEND);
// Без строки в vtiger_modcommentscf комментарий не попадёт в список (INNER JOIN в getListQuery)
$result3 = $adb->pquery('INSERT INTO vtiger_modcommentscf (modcommentsid) VALUES (?)', array($commentCrmId));
if (!$result3) {
$error = $adb->database->ErrorMsg();
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ❌ Ошибка INSERT vtiger_modcommentscf: ' . $error . PHP_EOL, FILE_APPEND);
echo json_encode(['success' => false, 'error' => 'DB error: modcommentscf - ' . $error]);
exit;
}
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ✅ vtiger_modcommentscf OK' . PHP_EOL, FILE_APPEND);
file_put_contents($logFile, date('Y-m-d H:i:s') . ' ✅ Создан комментарий ID ' . $commentCrmId . ' для контакта ' . $contact_id . PHP_EOL, FILE_APPEND);
$comment_id_ws = '20x' . $commentCrmId; // ModComments в vtiger обычно 20x
// Уведомление ответственного (всплывашка)
$notify_user_id = $owner_id;
if ($project_id > 0) {
$proj = $adb->pquery(
'SELECT e.smownerid FROM vtiger_project p INNER JOIN vtiger_crmentity e ON e.crmid = p.projectid
WHERE e.deleted = 0 AND p.projectid = ? AND p.linktoaccountscontacts = ? AND p.projectstatus <> ?',
array($project_id, $contact_id, 'completed')
);
if ($adb->num_rows($proj) === 1) {
$notify_user_id = $adb->query_result($proj, 0, 'smownerid');
}
}
$link = 'module=Contacts&view=Detail&record=' . $contact_id . '&app=MARKETING';
$title = 'Ответ поддержки добавлен';
$exist = $adb->pquery(
'SELECT id FROM vtiger_vdnotifierpro WHERE userid = ? AND crmid = ? AND title = ? AND status = 5',
array($notify_user_id, $contact_id, $title)
);
if ($adb->num_rows($exist) > 0) {
$id = $adb->query_result($exist, 0, 'id');
$adb->pquery('UPDATE vtiger_vdnotifierpro SET modifiedtime = ? WHERE id = ?', array($date_var, $id));
} else {
$adb->pquery(
'INSERT INTO vtiger_vdnotifierpro (userid, modulename, crmid, modiuserid, link, title, action, modifiedtime, status) VALUES (?, ?, ?, 0, ?, ?, "", ?, 5)',
array($notify_user_id, $setype, $contact_id, $link, $title, $date_var)
);
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => true,
'comment_id' => $comment_id_ws,
'contact_id' => $contact_id
]);