From d7982931cd6d0a42e127bc46efb38841d028382a Mon Sep 17 00:00:00 2001 From: Fedor Date: Tue, 3 Feb 2026 14:02:12 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20telegram=5Freplay.php=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BE=D1=82=D0=B2=D0=B5=D1=82=D0=BE=D0=B2=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=B2=20CRM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый 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 --- include/Webservices/CreateWebClaimV2.php | 288 +++++++++++++++++++++++ telegram_replay.php | 186 +++++++++++++++ 2 files changed, 474 insertions(+) create mode 100644 include/Webservices/CreateWebClaimV2.php create mode 100644 telegram_replay.php diff --git a/include/Webservices/CreateWebClaimV2.php b/include/Webservices/CreateWebClaimV2.php new file mode 100644 index 00000000..dddbc3dc --- /dev/null +++ b/include/Webservices/CreateWebClaimV2.php @@ -0,0 +1,288 @@ + 'Задержка рейса', + '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; +} diff --git a/telegram_replay.php b/telegram_replay.php new file mode 100644 index 00000000..697801cc --- /dev/null +++ b/telegram_replay.php @@ -0,0 +1,186 @@ + 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 +]);