diff --git a/include/Webservices/UpsertAccounts.php b/include/Webservices/UpsertAccounts.php new file mode 100644 index 00000000..7488e156 --- /dev/null +++ b/include/Webservices/UpsertAccounts.php @@ -0,0 +1,225 @@ + true, + 'total' => count($offenders), + 'created' => 0, + 'found' => 0, + 'errors' => 0, + 'accounts' => array() + ); + + // Обрабатываем каждого offender + foreach ($offenders as $index => $offender) { + $accountResult = array( + 'index' => $index, + 'success' => false, + 'account_id' => null, + 'action' => null, + 'accountname' => $offender['accountname'] ?? '', + 'inn' => $offender['inn'] ?? '', + 'role' => $offender['role'] ?? null, + 'message' => '' + ); + + try { + // Извлекаем данные + $accountname = trim($offender['accountname'] ?? ''); + $address = trim($offender['address'] ?? ''); + $email = trim($offender['email'] ?? ''); + $website = trim($offender['website'] ?? ''); + $phone = trim($offender['phone'] ?? ''); + $inn = preg_replace('/[^0-9]/', '', $offender['inn'] ?? ''); // Только цифры + $ogrn = preg_replace('/[^0-9]/', '', $offender['ogrn'] ?? ''); // Только цифры + $role = trim($offender['role'] ?? ''); + + // Проверка обязательных полей + if (empty($accountname)) { + throw new Exception('Не указано наименование контрагента (accountname)'); + } + if (empty($inn)) { + throw new Exception('Не указан ИНН'); + } + + // Валидация ИНН (10 или 12 цифр) + if (strlen($inn) != 10 && strlen($inn) != 12) { + $logstring = date('Y-m-d H:i:s') . " ⚠️ Нестандартный ИНН: $inn (длина " . strlen($inn) . ')'; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + // Не падаем, просто логируем + } + + // ======================================== + // ПОИСК ПО ИНН + // ======================================== + $query = "SELECT a.accountid, a.accountname + FROM vtiger_account a + LEFT JOIN vtiger_crmentity e ON e.crmid = a.accountid + WHERE e.deleted = 0 AND a.inn = ? + LIMIT 1"; + $res = $adb->pquery($query, array($inn)); + + if ($adb->num_rows($res) > 0) { + // === НАЙДЕН — просто возвращаем ID === + $existingId = $adb->query_result($res, 0, 'accountid'); + $existingName = $adb->query_result($res, 0, 'accountname'); + + $accountResult['success'] = true; + $accountResult['account_id'] = $existingId; + $accountResult['action'] = 'found'; + $accountResult['message'] = 'Контрагент найден по ИНН'; + $accountResult['existing_name'] = $existingName; + + $results['found']++; + + $logstring = date('Y-m-d H:i:s') . " ✓ [$index] Найден: $existingId ($existingName) по ИНН $inn"; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + } else { + // === НЕ НАЙДЕН — создаём === + $params = array( + 'accountname' => $accountname, + 'bill_street' => $address, + 'email1' => $email, + 'website' => $website, + 'phone' => $phone, + 'inn' => $inn, + 'cf_1951' => $ogrn, // ОГРН в кастомном поле + 'assigned_user_id' => vtws_getWebserviceEntityId('Users', $current_user->id) + ); + + $logstring = date('Y-m-d H:i:s') . " 🆕 [$index] Создаём: " . json_encode($params, JSON_UNESCAPED_UNICODE); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + $account = vtws_create('Accounts', $params, $current_user); + $newAccountId = substr($account['id'], 3); // Убираем 11x + + $accountResult['success'] = true; + $accountResult['account_id'] = $newAccountId; + $accountResult['action'] = 'created'; + $accountResult['message'] = 'Контрагент создан'; + + $results['created']++; + + $logstring = date('Y-m-d H:i:s') . " ✅ [$index] Создан: $newAccountId"; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + } + + } catch (WebServiceException $ex) { + $accountResult['success'] = false; + $accountResult['message'] = $ex->getMessage(); + $results['errors']++; + + $logstring = date('Y-m-d H:i:s') . " ❌ [$index] WebService ошибка: " . $ex->getMessage(); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + } catch (Exception $ex) { + $accountResult['success'] = false; + $accountResult['message'] = $ex->getMessage(); + $results['errors']++; + + $logstring = date('Y-m-d H:i:s') . " ❌ [$index] Ошибка: " . $ex->getMessage(); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + } + + $results['accounts'][] = $accountResult; + } + + // Итоговый статус + $results['success'] = ($results['errors'] == 0); + + $logstring = date('Y-m-d H:i:s') . ' RESULT: total=' . $results['total'] + . ', created=' . $results['created'] + . ', found=' . $results['found'] + . ', errors=' . $results['errors'] . PHP_EOL; + file_put_contents($logFile, $logstring, FILE_APPEND); + + return json_encode($results, JSON_UNESCAPED_UNICODE); +} \ No newline at end of file diff --git a/include/Webservices/UpsertContact.php b/include/Webservices/UpsertContact.php new file mode 100644 index 00000000..621fe34d --- /dev/null +++ b/include/Webservices/UpsertContact.php @@ -0,0 +1,235 @@ + false, + 'contact_id' => null, + 'action' => null, // 'created', 'updated', 'found' + 'message' => '' + ); + + // ======================================== + // 1. ФОРМАТИРОВАНИЕ ТЕЛЕФОНА + // ======================================== + if (!empty($mobile)) { + $mobile = preg_replace('/[^0-9]/', '', $mobile); + if (strlen($mobile) == 11 && $mobile[0] == '8') { + $mobile = "7" . substr($mobile, 1); + } else if (strlen($mobile) == 10) { + $mobile = "7" . $mobile; + } else if (strlen($mobile) != 11) { + // Некорректный номер - логируем, но не падаем + $logstring = date("Y-m-d H:i:s") . ' ⚠️ Некорректный номер телефона: ' . $mobile . ' (игнорируем)'; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + $mobile = ''; // Обнуляем некорректный номер + } + } + + // ======================================== + // 2. ПОИСК СУЩЕСТВУЮЩЕГО КОНТАКТА + // ======================================== + $existingContactId = null; + $searchMethod = ''; + + // 2.1 По contact_id (приоритет 1) + if (!empty($contact_id)) { + $contact_id = preg_replace('/[^0-9]/', '', $contact_id); // Очищаем от 12x префикса + $query = "SELECT c.contactid FROM vtiger_contactdetails c + LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid + WHERE e.deleted = 0 AND c.contactid = ? LIMIT 1"; + $res = $adb->pquery($query, array($contact_id)); + if ($adb->num_rows($res) > 0) { + $existingContactId = $adb->query_result($res, 0, 'contactid'); + $searchMethod = 'by_contact_id'; + } + } + + // 2.2 По mobile (приоритет 2) + if (empty($existingContactId) && !empty($mobile)) { + $query = "SELECT c.contactid FROM vtiger_contactdetails c + LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid + WHERE e.deleted = 0 AND c.mobile = ? LIMIT 1"; + $res = $adb->pquery($query, array($mobile)); + if ($adb->num_rows($res) > 0) { + $existingContactId = $adb->query_result($res, 0, 'contactid'); + $searchMethod = 'by_mobile'; + } + } + + // 2.3 По tgid (приоритет 3) - tgid хранится в поле phone + if (empty($existingContactId) && !empty($tgid)) { + $query = "SELECT c.contactid FROM vtiger_contactdetails c + LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid + WHERE e.deleted = 0 AND c.phone = ? LIMIT 1"; + $res = $adb->pquery($query, array($tgid)); + if ($adb->num_rows($res) > 0) { + $existingContactId = $adb->query_result($res, 0, 'contactid'); + $searchMethod = 'by_tgid'; + } + } + + $logstring = date('Y-m-d H:i:s') . ' Поиск: contact_id=' . $contact_id . ', mobile=' . $mobile . ', tgid=' . $tgid; + $logstring .= ' → Найден: ' . ($existingContactId ? $existingContactId . ' (' . $searchMethod . ')' : 'НЕТ'); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + // ======================================== + // 3. ФОРМИРУЕМ ПАРАМЕТРЫ + // ======================================== + $params = array(); + + // Только непустые поля добавляем в params + if (!empty($firstname)) $params['firstname'] = $firstname; + if (!empty($secondname)) $params['cf_1157'] = $secondname; // Отчество + if (!empty($lastname)) $params['lastname'] = $lastname; + if (!empty($mobile)) $params['mobile'] = $mobile; + if (!empty($email)) $params['email'] = $email; + if (!empty($tgid)) $params['phone'] = $tgid; // TG ID в поле phone + if (!empty($birthday)) $params['birthday'] = $birthday; + if (!empty($birthplace)) $params['cf_1263'] = $birthplace; // Место рождения + if (!empty($mailingstreet)) $params['mailingstreet'] = $mailingstreet; + if (!empty($inn)) $params['cf_1257'] = $inn; // ИНН + if (!empty($requisites)) $params['cf_1849'] = $requisites; // Реквизиты + if (!empty($code)) $params['cf_1580'] = $code; // SMS код + + // ======================================== + // 4. СОЗДАНИЕ ИЛИ ОБНОВЛЕНИЕ + // ======================================== + try { + if (!empty($existingContactId)) { + // === ОБНОВЛЕНИЕ === + $params['id'] = '12x' . $existingContactId; + + $logstring = date('Y-m-d H:i:s') . ' 📝 Обновляем контакт ' . $existingContactId . ': ' . json_encode($params); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + $contact = vtws_revise($params, $current_user); + + $result['success'] = true; + $result['contact_id'] = $existingContactId; + $result['action'] = 'updated'; + $result['search_method'] = $searchMethod; + $result['message'] = 'Контакт обновлён'; + + $logstring = date('Y-m-d H:i:s') . ' ✅ Контакт ' . $existingContactId . ' обновлён'; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + } else { + // === СОЗДАНИЕ === + + // Проверяем минимальные данные для создания + if (empty($mobile) && empty($tgid)) { + throw new WebServiceException( + WebServiceErrorCode::$INVALIDID, + "Для создания контакта нужен хотя бы mobile или tgid" + ); + } + + // Дефолтные значения для обязательных полей CRM + if (empty($params['firstname'])) { + $params['firstname'] = 'Клиент'; + } + if (empty($params['lastname'])) { + $suffix = !empty($mobile) ? substr($mobile, -4) : substr($tgid, -4); + $params['lastname'] = 'Web_' . $suffix; + } + if (empty($params['birthday'])) { + $params['birthday'] = '01-01-1990'; + } + + // Назначаем ответственного + $params['assigned_user_id'] = vtws_getWebserviceEntityId('Users', $current_user->id); + + $logstring = date('Y-m-d H:i:s') . ' 🆕 Создаём контакт: ' . json_encode($params); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + $contact = vtws_create('Contacts', $params, $current_user); + $newContactId = substr($contact['id'], 3); // Убираем 12x + + $result['success'] = true; + $result['contact_id'] = $newContactId; + $result['action'] = 'created'; + $result['message'] = 'Контакт создан'; + + $logstring = date('Y-m-d H:i:s') . ' ✅ Создан контакт ' . $newContactId; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + } + + } catch (WebServiceException $ex) { + $result['success'] = false; + $result['message'] = $ex->getMessage(); + + $logstring = date('Y-m-d H:i:s') . ' ❌ Ошибка: ' . $ex->getMessage(); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + throw $ex; + } + + $logstring = date('Y-m-d H:i:s') . ' RESULT: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL; + file_put_contents($logFile, $logstring, FILE_APPEND); + + return json_encode($result, JSON_UNESCAPED_UNICODE); +} \ No newline at end of file diff --git a/include/Webservices/UpsertProject.php b/include/Webservices/UpsertProject.php new file mode 100644 index 00000000..111e4897 --- /dev/null +++ b/include/Webservices/UpsertProject.php @@ -0,0 +1,297 @@ + false, + 'project_id' => null, + 'claim_id' => null, + 'action' => null, + 'offender_id' => null, + 'agent_id' => null, + 'message' => '' + ); + + // Извлекаем данные + $project_id = trim($data['project_id'] ?? ''); + $claim_id = trim($data['claim_id'] ?? ''); + $contact_id = trim($data['contact_id'] ?? ''); + $projectdata = $data['projectdata'] ?? []; + + // Извлекаем контрагентов из result (если передан) или из offender_ids + $offender_ids = []; + + if (!empty($data['result'])) { + // Парсим result от UpsertAccounts + $accountsResult = $data['result']; + if (is_string($accountsResult)) { + $accountsResult = json_decode($accountsResult, true); + } + + // Извлекаем account_id из accounts[] + if (isset($accountsResult['accounts']) && is_array($accountsResult['accounts'])) { + foreach ($accountsResult['accounts'] as $account) { + if (!empty($account['account_id'])) { + $offender_ids[] = $account['account_id']; + } + } + } + + $logstring = date('Y-m-d H:i:s') . ' Извлечены offender_ids из result: ' . json_encode($offender_ids); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + } elseif (!empty($data['offender_ids'])) { + $offender_ids = $data['offender_ids']; + } + + // cf_2274 = первый контрагент (основной ответчик) + // cf_2276 = второй контрагент (агент/второй ответчик) + $offender_id = count($offender_ids) > 0 ? $offender_ids[0] : ''; + $agent_id = count($offender_ids) > 1 ? $offender_ids[1] : ''; + + $logstring = date('Y-m-d H:i:s') . " Данные: project_id=$project_id, claim_id=$claim_id, contact_id=$contact_id, offender_id=$offender_id, agent_id=$agent_id"; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + try { + // ======================================== + // ПРОВЕРКА СУЩЕСТВОВАНИЯ ПРОЕКТА + // ======================================== + $existingProjectId = null; + + if (!empty($project_id)) { + $project_id = preg_replace('/[^0-9]/', '', $project_id); + $query = "SELECT p.projectid FROM vtiger_project p + LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid + WHERE e.deleted = 0 AND p.projectid = ? LIMIT 1"; + $res = $adb->pquery($query, array($project_id)); + if ($adb->num_rows($res) > 0) { + $existingProjectId = $adb->query_result($res, 0, 'projectid'); + } + } + + // ======================================== + // ФОРМИРУЕМ ПАРАМЕТРЫ + // ======================================== + $params = array(); + + // Если создаём новый проект - нужны contact_id и offender_id + if (empty($existingProjectId)) { + if (empty($contact_id) || empty($offender_id)) { + throw new Exception('Для создания проекта нужны contact_id и offender_ids'); + } + + // Получаем название контакта + $query = "SELECT c.lastname FROM vtiger_contactdetails c + LEFT JOIN vtiger_crmentity e ON e.crmid = c.contactid + WHERE e.deleted = 0 AND c.contactid = ? LIMIT 1"; + $res = $adb->pquery($query, array($contact_id)); + $contactName = $adb->num_rows($res) > 0 ? $adb->query_result($res, 0, 'lastname') : 'Клиент'; + + // Получаем название контрагента + $query = "SELECT a.accountname FROM vtiger_account a + LEFT JOIN vtiger_crmentity e ON e.crmid = a.accountid + WHERE e.deleted = 0 AND a.accountid = ? LIMIT 1"; + $res = $adb->pquery($query, array($offender_id)); + $accountName = $adb->num_rows($res) > 0 ? $adb->query_result($res, 0, 'accountname') : 'Контрагент'; + + // Название проекта + $params['projectname'] = $contactName . ' ' . $accountName; + $params['linktoaccountscontacts'] = '12x' . $contact_id; + $params['cf_2274'] = '11x' . $offender_id; // Основной ответчик + $params['projectstatus'] = 'модерация'; + $params['projecttype'] = 'Претензионно - исковая работа'; + $params['assigned_user_id'] = vtws_getWebserviceEntityId('Users', $current_user->id); + + // Заявитель по умолчанию + if (!isset($projectdata['cf_1994'])) { + $params['cf_1994'] = vtws_getWebserviceEntityId('Accounts', 62345); // МОО КЛИЕНТПРАВ + } + } + + // Агент (второй ответчик) + if (!empty($agent_id)) { + $params['cf_2276'] = '11x' . $agent_id; + } + + // Связь контакт/оффендер для обновления тоже можно передать + if (!empty($contact_id) && !empty($existingProjectId)) { + $params['linktoaccountscontacts'] = '12x' . $contact_id; + } + if (!empty($offender_id) && !empty($existingProjectId)) { + $params['cf_2274'] = '11x' . $offender_id; + } + + // Маппинг полей из projectdata + $fieldMapping = array( + 'cf_2206' => 'cf_2206', // SMS код + 'cf_2210' => 'cf_2210', // IP + 'cf_2212' => 'cf_2212', // Источник + 'cf_2214' => 'cf_2214', // Регион + 'cf_2208' => 'cf_2208', // Form ID + 'cf_1830' => 'cf_1830', // Категория + 'cf_1469' => 'cf_1469', // Направление + 'cf_1191' => 'cf_1191', // Цена договора + 'cf_1189' => 'cf_1189', // Предмет договора + 'cf_1203' => 'cf_1203', // Дата договора + 'cf_1839' => 'cf_1839', // Дата начала + 'cf_1841' => 'cf_1841', // Дата окончания + 'cf_1207' => 'cf_1207', // Ущерб + 'cf_1479' => 'cf_1479', // Стоимость услуг + 'cf_1227' => 'cf_1227', // Прогресс + 'cf_1231' => 'cf_1231', // Страна + 'cf_1239' => 'cf_1239', // Отель + 'cf_1566' => 'cf_1566', // Транспорт + 'cf_1564' => 'cf_1564', // Страховка + 'cf_1249' => 'cf_1249', // Прочее + 'cf_1471' => 'cf_1471', // Самостоятельно + 'cf_1473' => 'cf_1473', // Дата претензии + 'cf_1475' => 'cf_1475', // Возвращено + 'cf_1994' => 'cf_1994', // Заявитель + 'description' => 'description' + ); + + foreach ($fieldMapping as $input => $crm) { + if (isset($projectdata[$input]) && $projectdata[$input] !== null) { + $value = $projectdata[$input]; + // Для cf_1994 (Заявитель) нужен формат 11xID + if ($crm === 'cf_1994' && !empty($value) && strpos($value, 'x') === false) { + $value = vtws_getWebserviceEntityId('Accounts', $value); + } + $params[$crm] = $value; + } + } + + // ======================================== + // СОЗДАНИЕ ИЛИ ОБНОВЛЕНИЕ + // ======================================== + if (!empty($existingProjectId)) { + // === ОБНОВЛЕНИЕ === + $params['id'] = '33x' . $existingProjectId; // 33x для Project + + $logstring = date('Y-m-d H:i:s') . ' 📝 Обновляем проект ' . $existingProjectId . ': ' . json_encode($params, JSON_UNESCAPED_UNICODE); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + $project = vtws_revise($params, $current_user); + + $result['success'] = true; + $result['project_id'] = $existingProjectId; + $result['claim_id'] = $claim_id; + $result['action'] = 'updated'; + $result['offender_id'] = $offender_id; + $result['agent_id'] = $agent_id ?: null; + $result['message'] = 'Проект обновлён'; + + $logstring = date('Y-m-d H:i:s') . ' ✅ Проект ' . $existingProjectId . ' обновлён'; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + } else { + // === СОЗДАНИЕ === + $logstring = date('Y-m-d H:i:s') . ' 🆕 Создаём проект: ' . json_encode($params, JSON_UNESCAPED_UNICODE); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + $project = vtws_create('Project', $params, $current_user); + $newProjectId = substr($project['id'], strpos($project['id'], 'x') + 1); // Убираем префикс (63x) + + $result['success'] = true; + $result['project_id'] = $newProjectId; + $result['claim_id'] = $claim_id; + $result['action'] = 'created'; + $result['offender_id'] = $offender_id; + $result['agent_id'] = $agent_id ?: null; + $result['message'] = 'Проект создан'; + + $logstring = date('Y-m-d H:i:s') . ' ✅ Создан проект ' . $newProjectId; + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + } + + } catch (WebServiceException $ex) { + $result['success'] = false; + $result['message'] = $ex->getMessage(); + + $logstring = date('Y-m-d H:i:s') . ' ❌ WebService ошибка: ' . $ex->getMessage(); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + throw $ex; + + } catch (Exception $ex) { + $result['success'] = false; + $result['message'] = $ex->getMessage(); + + $logstring = date('Y-m-d H:i:s') . ' ❌ Ошибка: ' . $ex->getMessage(); + file_put_contents($logFile, $logstring . PHP_EOL, FILE_APPEND); + + throw new WebServiceException(WebServiceErrorCode::$INVALIDID, $ex->getMessage()); + } + + $logstring = date('Y-m-d H:i:s') . ' RESULT: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL; + file_put_contents($logFile, $logstring, FILE_APPEND); + + return json_encode($result, JSON_UNESCAPED_UNICODE); +} diff --git a/ticket_form/backend/app/api/claims.py b/ticket_form/backend/app/api/claims.py index 4bbc25e0..9d2d3735 100644 --- a/ticket_form/backend/app/api/claims.py +++ b/ticket_form/backend/app/api/claims.py @@ -316,18 +316,68 @@ async def list_drafts( else: payload = {} + # Извлекаем данные из ai_analysis или wizard_plan + ai_analysis = payload.get('ai_analysis') or {} + wizard_plan = payload.get('wizard_plan') or {} + + # Краткое описание проблемы (заголовок) + problem_title = ai_analysis.get('problem') or payload.get('problem') or None + + # Категория проблемы + category = ai_analysis.get('category') or wizard_plan.get('category') or None + + # Подробное описание (для превью) + problem_text = payload.get('problem_description', '') + + # Считаем документы + documents_meta = payload.get('documents_meta') or [] + documents_required = payload.get('documents_required') or [] + + # Считаем загруженные (уникальные по field_label) + uploaded_labels = set() + for doc in documents_meta: + label = doc.get('field_label') or doc.get('field_name') + if label: + uploaded_labels.add(label) + + documents_uploaded = len(uploaded_labels) + documents_total = len(documents_required) if documents_required else 0 + + # Формируем список документов со статусами + documents_list = [] + for doc_req in documents_required: + doc_name = doc_req.get('name', 'Документ') + doc_id = doc_req.get('id', '') + is_required = doc_req.get('required', False) + # Проверяем загружен ли (по name или id) + is_uploaded = doc_name in uploaded_labels or doc_id in uploaded_labels + documents_list.append({ + "name": doc_name, + "required": is_required, + "uploaded": is_uploaded, + }) + drafts.append({ "id": str(row['id']), "claim_id": row.get('claim_id'), "session_token": row.get('session_token'), "status_code": row.get('status_code'), - "channel": row.get('channel'), # Добавляем канал в ответ + "channel": row.get('channel'), "created_at": row['created_at'].isoformat() if row.get('created_at') else None, "updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None, - "problem_description": payload.get('problem_description', '')[:100] if payload.get('problem_description') else None, + # Заголовок - краткое описание проблемы из AI + "problem_title": problem_title[:150] if problem_title else None, + # Полное описание + "problem_description": problem_text[:500] if problem_text else None, + "category": category, "wizard_plan": payload.get('wizard_plan') is not None, "wizard_answers": payload.get('answers') is not None, - "has_documents": len(payload.get('documents_meta', [])) > 0 if payload.get('documents_meta') else False, + "has_documents": documents_uploaded > 0, + # Прогресс документов + "documents_total": documents_total, + "documents_uploaded": documents_uploaded, + "documents_skipped": 0, # TODO: считать пропущенные + "documents_list": documents_list, # Список со статусами }) return { diff --git a/ticket_form/backend/app/api/documents.py b/ticket_form/backend/app/api/documents.py index 0723247f..6a35796a 100644 --- a/ticket_form/backend/app/api/documents.py +++ b/ticket_form/backend/app/api/documents.py @@ -22,6 +22,22 @@ logger = logging.getLogger(__name__) N8N_DOCUMENT_UPLOAD_WEBHOOK = "https://n8n.clientright.pro/webhook/webform_document_upload" +def get_client_ip(request: Request) -> str: + """Получить реальный IP клиента (с учётом proxy заголовков)""" + # Сначала проверяем заголовки от reverse proxy + forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() + real_ip = request.headers.get("x-real-ip", "").strip() + + # X-Forwarded-For имеет приоритет + if forwarded_for and forwarded_for not in ("127.0.0.1", "192.168.0.1", "::1"): + return forwarded_for + if real_ip and real_ip not in ("127.0.0.1", "192.168.0.1", "::1"): + return real_ip + + # Fallback на request.client + return request.client.host if request.client else "unknown" + + @router.post("/upload") async def upload_document( request: Request, @@ -67,10 +83,7 @@ async def upload_document( file_size = len(file_content) # Получаем IP клиента - client_ip = request.client.host if request.client else "unknown" - forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() - if forwarded_for: - client_ip = forwarded_for + client_ip = get_client_ip(request) # Формируем данные в формате совместимом с существующим n8n воркфлоу form_data = { @@ -223,10 +236,7 @@ async def upload_multiple_documents( ) # Получаем IP клиента - client_ip = request.client.host if request.client else "unknown" - forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() - if forwarded_for: - client_ip = forwarded_for + client_ip = get_client_ip(request) # Генерируем ID для каждого файла и читаем контент file_ids = [] @@ -426,10 +436,7 @@ async def skip_document( ) # Получаем IP клиента - client_ip = request.client.host if request.client else "unknown" - forwarded_for = request.headers.get("x-forwarded-for", "").split(",")[0].strip() - if forwarded_for: - client_ip = forwarded_for + client_ip = get_client_ip(request) # Формируем данные в формате совместимом с существующим n8n воркфлоу form_data = { diff --git a/ticket_form/backend/app/api/events.py b/ticket_form/backend/app/api/events.py index 88240a43..7fe452c9 100644 --- a/ticket_form/backend/app/api/events.py +++ b/ticket_form/backend/app/api/events.py @@ -13,7 +13,7 @@ import logging logger = logging.getLogger(__name__) -router = APIRouter() +router = APIRouter(prefix="/api/v1", tags=["Events"]) class EventPublish(BaseModel): @@ -215,11 +215,71 @@ async def stream_events(task_id: str): except Exception as e: logger.error(f"❌ Error loading wizard data from PostgreSQL: {e}") + # ✅ Обработка ocr_status ready: загружаем form_draft из PostgreSQL + if actual_event.get('event_type') == 'ocr_status' and actual_event.get('status') == 'ready': + claim_id = actual_event.get('claim_id') or actual_event.get('data', {}).get('claim_id') + if claim_id: + logger.info(f"🔍 OCR ready event received, loading form_draft for claim_id={claim_id}") + + try: + # Загружаем form_draft и documents из PostgreSQL + query = """ + SELECT + c.id, + c.payload->'form_draft' as form_draft, + c.payload->'documents_required' as documents_required, + c.payload->'documents_meta' as documents_meta + FROM clpr_claims c + WHERE c.id::text = $1 + LIMIT 1 + """ + + row = await db.fetch_one(query, claim_id) + + if row: + # Парсим JSONB поля (могут быть строками) + form_draft_raw = row.get('form_draft') + documents_required_raw = row.get('documents_required') + documents_meta_raw = row.get('documents_meta') + + # Парсим если строка + def parse_json_field(val): + if val is None: + return None + if isinstance(val, str): + try: + return json.loads(val) + except: + return val + return val + + form_draft = parse_json_field(form_draft_raw) + documents_required = parse_json_field(documents_required_raw) + documents_meta = parse_json_field(documents_meta_raw) + + # Обогащаем событие данными из БД + actual_event['data'] = { + 'claim_id': claim_id, + 'all_ready': True, + 'form_draft': form_draft, + 'documents_required': documents_required, + 'documents_meta': documents_meta, + } + + logger.info(f"✅ Form draft loaded from PostgreSQL for claim_id={claim_id}, has_form_draft={form_draft is not None}") + else: + logger.warning(f"⚠️ Claim not found in PostgreSQL: claim_id={claim_id}") + except Exception as e: + logger.error(f"❌ Error loading form_draft from PostgreSQL: {e}") + # Отправляем событие клиенту (плоский формат) - event_json = json.dumps(actual_event, ensure_ascii=False) + event_json = json.dumps(actual_event, ensure_ascii=False, default=str) event_type_sent = actual_event.get('event_type', 'unknown') event_status = actual_event.get('status', 'unknown') - logger.info(f"📤 Sending event to client: type={event_type_sent}, status={event_status}") + # Логируем размер и наличие данных + data_info = actual_event.get('data', {}) + has_form_draft = 'form_draft' in data_info if isinstance(data_info, dict) else False + logger.info(f"📤 Sending event to client: type={event_type_sent}, status={event_status}, json_len={len(event_json)}, has_form_draft={has_form_draft}") yield f"data: {event_json}\n\n" # Если обработка завершена - закрываем соединение @@ -232,6 +292,11 @@ async def stream_events(task_id: str): if event_type_sent in ['claim_ready', 'claim_plan_ready']: logger.info(f"✅ Final event {event_type_sent} sent, closing SSE") break + + # Закрываем для ocr_status ready (форма заявления готова) + if event_type_sent == 'ocr_status' and event_status == 'ready': + logger.info(f"✅ OCR ready event sent, closing SSE") + break else: logger.info(f"⏰ Timeout waiting for message on {channel}") diff --git a/ticket_form/frontend/index.html b/ticket_form/frontend/index.html index 05d12287..9ba69bc4 100644 --- a/ticket_form/frontend/index.html +++ b/ticket_form/frontend/index.html @@ -4,7 +4,7 @@ -