fix: Интеграция n8n webhook для создания контакта после SMS
- Step1Phone теперь вызывает n8n webhook после SMS верификации - Webhook создаёт/находит контакт в CRM через CreateWebContact - Возвращает: contact_id, claim_id, is_new_contact - Данные сохраняются в formData для дальнейшей работы - Исправлена нормализация телефона в sms_service (убираем +) - Отключен rate limiting SMS для тестирования - Backend подключён к внешнему Redis (crm.clientright.ru:6379) - Добавлены поля contact_id, is_new_contact в FormData - Frontend пересобран с новым кодом
This commit is contained in:
@@ -121,13 +121,17 @@ class SMSService:
|
|||||||
Returns:
|
Returns:
|
||||||
str: Код верификации (для отладки) или None при ошибке
|
str: Код верификации (для отладки) или None при ошибке
|
||||||
"""
|
"""
|
||||||
|
# Нормализуем формат телефона (убираем + если есть)
|
||||||
|
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
|
||||||
|
|
||||||
# Проверка rate limiting (не больше 1 SMS в минуту на номер)
|
# Проверка rate limiting (не больше 1 SMS в минуту на номер)
|
||||||
|
# ВРЕМЕННО ОТКЛЮЧЕНО для тестирования
|
||||||
rate_limit_key = f"sms_rate:{phone}"
|
rate_limit_key = f"sms_rate:{phone}"
|
||||||
|
|
||||||
if await redis_service.exists(rate_limit_key):
|
# if await redis_service.exists(rate_limit_key):
|
||||||
ttl = await redis_service.client.ttl(f"{settings.redis_prefix}{rate_limit_key}")
|
# ttl = await redis_service.client.ttl(f"{settings.redis_prefix}{rate_limit_key}")
|
||||||
logger.warning(f"Rate limit for {phone}, retry in {ttl} seconds")
|
# logger.warning(f"Rate limit for {phone}, retry in {ttl} seconds")
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
# Генерируем код
|
# Генерируем код
|
||||||
code = self.generate_code()
|
code = self.generate_code()
|
||||||
@@ -137,7 +141,8 @@ class SMSService:
|
|||||||
await redis_service.set(verification_key, code, expire=600) # 10 минут
|
await redis_service.set(verification_key, code, expire=600) # 10 минут
|
||||||
|
|
||||||
# Устанавливаем rate limit на 60 секунд
|
# Устанавливаем rate limit на 60 секунд
|
||||||
await redis_service.set(rate_limit_key, "1", expire=60)
|
# ВРЕМЕННО ОТКЛЮЧЕНО для тестирования - убрать задержку
|
||||||
|
# await redis_service.set(rate_limit_key, "1", expire=60)
|
||||||
|
|
||||||
# Формируем сообщение
|
# Формируем сообщение
|
||||||
message = f"Ваш код подтверждения: {code}. Действителен 10 минут."
|
message = f"Ваш код подтверждения: {code}. Действителен 10 минут."
|
||||||
@@ -164,20 +169,25 @@ class SMSService:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True если код верный
|
bool: True если код верный
|
||||||
"""
|
"""
|
||||||
|
# Нормализуем формат телефона (убираем + если есть)
|
||||||
|
phone = phone.replace("+", "").replace("-", "").replace(" ", "")
|
||||||
|
|
||||||
verification_key = f"sms_verify:{phone}"
|
verification_key = f"sms_verify:{phone}"
|
||||||
stored_code = await redis_service.get(verification_key)
|
stored_code = await redis_service.get(verification_key)
|
||||||
|
|
||||||
if not stored_code:
|
if not stored_code:
|
||||||
logger.warning(f"No verification code found for {phone}")
|
logger.warning(f"No verification code found for {phone} (key: {verification_key})")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
logger.info(f"🔍 Comparing codes: stored='{stored_code}' vs input='{code}' (types: {type(stored_code).__name__} vs {type(code).__name__})")
|
||||||
|
|
||||||
if stored_code == code:
|
if stored_code == code:
|
||||||
# Удаляем код после успешной проверки
|
# Удаляем код после успешной проверки
|
||||||
await redis_service.delete(verification_key)
|
await redis_service.delete(verification_key)
|
||||||
logger.info(f"✅ Code verified for {phone}")
|
logger.info(f"✅ Code verified for {phone}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Invalid code for {phone}")
|
logger.warning(f"❌ Invalid code for {phone}: expected '{stored_code}', got '{code}'")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8100:8100"
|
- "8100:8100"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_HOST=crm.clientright.ru
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_PASSWORD=CRM_Redis_Pass_2025_Secure!
|
||||||
- POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db
|
- POSTGRES_URL=postgresql://erv_user:erv_password@postgres:5432/erv_db
|
||||||
- RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672
|
- RABBITMQ_URL=amqp://admin:tyejvtej@185.197.75.249:5672
|
||||||
depends_on:
|
# depends_on:
|
||||||
- redis
|
# - redis
|
||||||
- postgres
|
# - postgres
|
||||||
networks:
|
networks:
|
||||||
- erv-network
|
- erv-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -86,7 +86,40 @@ export default function Step1Phone({
|
|||||||
addDebugEvent?.('sms', 'success', `✅ Телефон подтвержден успешно`, { phone, verified: true });
|
addDebugEvent?.('sms', 'success', `✅ Телефон подтвержден успешно`, { phone, verified: true });
|
||||||
message.success('Телефон подтвержден!');
|
message.success('Телефон подтвержден!');
|
||||||
setIsPhoneVerified(true);
|
setIsPhoneVerified(true);
|
||||||
onNext();
|
|
||||||
|
// После верификации создаём контакт в CRM через n8n
|
||||||
|
try {
|
||||||
|
addDebugEvent?.('crm', 'info', '📞 Создание контакта в CRM...', { phone });
|
||||||
|
|
||||||
|
const crmResponse = await fetch('https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const crmResult = await crmResponse.json();
|
||||||
|
|
||||||
|
if (crmResponse.ok) {
|
||||||
|
addDebugEvent?.('crm', 'success', `✅ Контакт создан/найден в CRM`, crmResult);
|
||||||
|
|
||||||
|
// Сохраняем данные из CRM в форму
|
||||||
|
updateFormData({
|
||||||
|
phone,
|
||||||
|
contact_id: crmResult.contact_id,
|
||||||
|
claim_id: crmResult.claim_id,
|
||||||
|
is_new_contact: crmResult.is_new_contact
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success(crmResult.is_new_contact ? 'Контакт создан!' : 'Контакт найден!');
|
||||||
|
onNext();
|
||||||
|
} else {
|
||||||
|
addDebugEvent?.('crm', 'error', '❌ Ошибка создания контакта в CRM', crmResult);
|
||||||
|
message.error('Ошибка создания контакта в CRM');
|
||||||
|
}
|
||||||
|
} catch (crmError) {
|
||||||
|
addDebugEvent?.('crm', 'error', '❌ Ошибка соединения с CRM', { error: String(crmError) });
|
||||||
|
message.error('Ошибка соединения с CRM');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, { phone, code, error: result.detail });
|
addDebugEvent?.('sms', 'error', `❌ Неверный код SMS`, { phone, code, error: result.detail });
|
||||||
message.error(result.detail || 'Неверный код');
|
message.error(result.detail || 'Неверный код');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback } from 'react';
|
||||||
import { Steps, Card, message, Row, Col } from 'antd';
|
import { Steps, Card, message, Row, Col } from 'antd';
|
||||||
|
import Step1Phone from '../components/form/Step1Phone';
|
||||||
import Step1Policy from '../components/form/Step1Policy';
|
import Step1Policy from '../components/form/Step1Policy';
|
||||||
import Step2EventType from '../components/form/Step2EventType';
|
import Step2EventType from '../components/form/Step2EventType';
|
||||||
import StepDocumentUpload from '../components/form/StepDocumentUpload';
|
import StepDocumentUpload from '../components/form/StepDocumentUpload';
|
||||||
@@ -11,15 +12,20 @@ import './ClaimForm.css';
|
|||||||
const { Step } = Steps;
|
const { Step } = Steps;
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
// Шаг 1: Policy
|
// Шаг 1: Phone
|
||||||
|
phone?: string;
|
||||||
|
contact_id?: string;
|
||||||
|
is_new_contact?: boolean;
|
||||||
|
|
||||||
|
// Шаг 2: Policy
|
||||||
voucher: string;
|
voucher: string;
|
||||||
claim_id?: string;
|
claim_id?: string;
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
|
|
||||||
// Шаг 2: Event Type
|
// Шаг 3: Event Type
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
|
|
||||||
// Шаги 3+: Documents
|
// Шаги 4+: Documents
|
||||||
documents?: Record<string, {
|
documents?: Record<string, {
|
||||||
uploaded: boolean;
|
uploaded: boolean;
|
||||||
data: any;
|
data: any;
|
||||||
@@ -30,7 +36,6 @@ interface FormData {
|
|||||||
// Последний шаг: Payment
|
// Последний шаг: Payment
|
||||||
fullName?: string;
|
fullName?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
|
||||||
paymentMethod?: string;
|
paymentMethod?: string;
|
||||||
bankName?: string;
|
bankName?: string;
|
||||||
cardNumber?: string;
|
cardNumber?: string;
|
||||||
@@ -165,7 +170,24 @@ export default function ClaimForm() {
|
|||||||
const steps = useMemo(() => {
|
const steps = useMemo(() => {
|
||||||
const stepsArray: any[] = [];
|
const stepsArray: any[] = [];
|
||||||
|
|
||||||
// Шаг 1: Policy (всегда)
|
// Шаг 1: Phone (телефон + SMS верификация)
|
||||||
|
stepsArray.push({
|
||||||
|
title: 'Телефон',
|
||||||
|
description: 'Подтверждение по SMS',
|
||||||
|
content: (
|
||||||
|
<Step1Phone
|
||||||
|
formData={{ ...formData, claim_id: claimId, session_id: sessionId }}
|
||||||
|
updateFormData={updateFormData}
|
||||||
|
onNext={nextStep}
|
||||||
|
onPrev={prevStep}
|
||||||
|
isPhoneVerified={isPhoneVerified}
|
||||||
|
setIsPhoneVerified={setIsPhoneVerified}
|
||||||
|
addDebugEvent={addDebugEvent}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Шаг 2: Policy (всегда)
|
||||||
stepsArray.push({
|
stepsArray.push({
|
||||||
title: 'Проверка полиса',
|
title: 'Проверка полиса',
|
||||||
description: 'Полис ERV',
|
description: 'Полис ERV',
|
||||||
|
|||||||
Reference in New Issue
Block a user