target fix
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(select-object *)",
|
||||||
|
"Bash(mvnw.cmd *)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\api-gateway-konturAi/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\konturai-front/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\api-gateway-konturAi\\src\\main\\java\\konturai\\kz\\api_gateway\\configuration/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\marketing/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src\\service/**)",
|
||||||
|
"Read(c:\\Users\\777\\IdeaProjects\\marketing\\src\\config/**)",
|
||||||
|
"Bash(findstr *)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"$version": 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(select-object *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Рефакторинг Маркетинговой Стратегии - Changelog
|
||||||
|
|
||||||
|
## Дата: 11 апреля 2026
|
||||||
|
|
||||||
|
### 📋 Описание изменений
|
||||||
|
|
||||||
|
Полностью переработана логика генерации маркетинговой стратегии для решения проблем:
|
||||||
|
- Недостаточное количество постов (было 8, стало 16)
|
||||||
|
- Неравномерное распределение постов (пропуски 2-3 дня)
|
||||||
|
- Отсутствие связи между постами и неделями
|
||||||
|
- Непрофессиональное отображение на фронте
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Что изменено
|
||||||
|
|
||||||
|
#### 1. **Увеличено количество постов: 8 → 16**
|
||||||
|
- **Было:** 8 постов за 4 недели (2 поста/неделю)
|
||||||
|
- **Стало:** 16 постов за 4 недели (4 поста/неделю)
|
||||||
|
- **Распределение:** Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00
|
||||||
|
|
||||||
|
#### 2. **Добавлено поле `weekNumber`**
|
||||||
|
- **Model:** `MarketingStrategy.PostCalendarItem.weekNumber`
|
||||||
|
- **DTO:** `MarketingStrategyResponse.PostCalendarItem.weekNumber`
|
||||||
|
- **Цель:** Чёткая связь каждого поста с конкретной неделей стратегии
|
||||||
|
|
||||||
|
#### 3. **Нормализация расписания**
|
||||||
|
- Новый метод: `normalizePostSchedule()`
|
||||||
|
- Автоматически выравнивает посты по дням недели
|
||||||
|
- Гарантирует равномерный ритм публикаций
|
||||||
|
- Сохраняет валидные даты от AI (если в пределах ±2 дней)
|
||||||
|
|
||||||
|
#### 4. **Усиленный промпт для AI**
|
||||||
|
- Чёткие инструкции по распределению 16 постов
|
||||||
|
- Примеры правильного JSON с `weekNumber`
|
||||||
|
- Явные требования: 4 поста/неделю, Пн-Ср-Пт-Сб
|
||||||
|
- Увеличено количество видео: 2 → 4 (по 1 в неделю)
|
||||||
|
|
||||||
|
#### 5. **Улучшенная валидация**
|
||||||
|
- Логирование при < 12 постах от AI
|
||||||
|
- Авто-расчёт дат если AI вернул некорректные
|
||||||
|
- Нормализация `weekNumber` для всех постов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📁 Изменённые файлы
|
||||||
|
|
||||||
|
1. **`MarketingStrategy.java`** (Model)
|
||||||
|
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||||
|
|
||||||
|
2. **`MarketingStrategyResponse.java`** (DTO)
|
||||||
|
- Добавлено поле `weekNumber` в `PostCalendarItem`
|
||||||
|
- Добавлены геттеры/сеттеры + поля `videoUrl`, `videoFilename`
|
||||||
|
|
||||||
|
3. **`MarketingStrategyV3Service.java`** (Service)
|
||||||
|
- Усилен `buildUserPrompt()` — детальные инструкции по распределению
|
||||||
|
- Переписан `populateStrategyEntity()` — обработка `weekNumber`
|
||||||
|
- Новый метод `normalizePostSchedule()` — выравнивание расписания
|
||||||
|
- Новый метод `getDayOffset()` — расчёт дней недели
|
||||||
|
- Обновлён `buildPostCountPlan()` — 16 постов вместо 12
|
||||||
|
- Увеличен лимит видео: 2 → 4
|
||||||
|
|
||||||
|
4. **`getStrategyResult()`**
|
||||||
|
- Добавлено поле `weekNumber` в ответ API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🎯 Результат
|
||||||
|
|
||||||
|
#### **До рефакторинга:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postCalendar": [
|
||||||
|
{ "publishDate": "2026-04-10T09:00:00", "theme": "Кейс: ..." },
|
||||||
|
{ "publishDate": "2026-04-12T18:00:00", "theme": "Процесс: ..." },
|
||||||
|
{ "publishDate": "2026-04-15T12:00:00", "theme": "Отзыв: ..." }
|
||||||
|
// ... всего 8 постов, пропуски 2-3 дня
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **После рефакторинга:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"postCalendar": [
|
||||||
|
{
|
||||||
|
"postIndex": 0,
|
||||||
|
"publishDate": "2026-04-13T09:00:00",
|
||||||
|
"weekNumber": 1,
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "видео",
|
||||||
|
"theme": "Кейс: Как мы увеличили продажи на 40%",
|
||||||
|
"publishTime": "09:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"postIndex": 1,
|
||||||
|
"publishDate": "2026-04-15T12:00:00",
|
||||||
|
"weekNumber": 1,
|
||||||
|
"platform": "Instagram",
|
||||||
|
"contentType": "фото",
|
||||||
|
"theme": "Процесс: За кадром нашей работы",
|
||||||
|
"publishTime": "12:00"
|
||||||
|
},
|
||||||
|
// ... ещё 14 постов, ровно 4 в неделю
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 Расписание постов (пример)
|
||||||
|
|
||||||
|
| Неделя | День | Время | Тип контента |
|
||||||
|
|--------|------|-------|--------------|
|
||||||
|
| 1 | Понедельник (день 1) | 09:00 | 🎥 Видео (Кейс) |
|
||||||
|
| 1 | Среда (день 3) | 12:00 | 📸 Фото (Процесс) |
|
||||||
|
| 1 | Пятница (день 5) | 18:00 | 📸 Фото (Отзыв) |
|
||||||
|
| 1 | Суббота (день 6) | 20:00 | 📸 Фото (Экспертный) |
|
||||||
|
| 2 | Понедельник (день 8) | 09:00 | 🎥 Видео (Кейс) |
|
||||||
|
| 2 | Среда (день 10) | 12:00 | 📸 Фото (Процесс) |
|
||||||
|
| ... | ... | ... | ... |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🚀 Преимущества
|
||||||
|
|
||||||
|
✅ **Профессиональный вид** — равномерное расписание, как у SMM-агентств
|
||||||
|
✅ **Предсказуемость** — клиент знает когда будет каждый пост
|
||||||
|
✅ **Связность** — каждый пост привязан к неделе и теме
|
||||||
|
✅ **Гибкость** — AI может предлагать даты, но система нормализует
|
||||||
|
✅ **Масштабируемость** — легко изменить на 3 или 5 постов/неделю
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚠️ Важно для фронтенда
|
||||||
|
|
||||||
|
Теперь в ответе API появляется новое поле `weekNumber` у каждого поста:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PostCalendarItem {
|
||||||
|
postIndex: number;
|
||||||
|
publishDate: string;
|
||||||
|
weekNumber: number; // ← НОВОЕ ПОЛЕ
|
||||||
|
platform: string;
|
||||||
|
contentType: string;
|
||||||
|
theme: string;
|
||||||
|
postText: string;
|
||||||
|
hashtags: string[];
|
||||||
|
publishTime: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
videoUrl?: string;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Рекомендуется использовать `weekNumber` для группировки постов на фронте!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🧪 Тестирование
|
||||||
|
|
||||||
|
После деплоя проверьте:
|
||||||
|
1. Генерацию новой стратегии — должно быть 16 постов
|
||||||
|
2. Распределение по неделям — по 4 поста в каждой
|
||||||
|
3. Даты публикаций — Пн-Ср-Пт-Сб
|
||||||
|
4. Поле `weekNumber` — должно присутствовать у всех постов
|
||||||
|
5. Видео-посты — ровно 4 (по 1 в неделю)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 Компиляция
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd c:\Users\777\IdeaProjects\telecomBackend
|
||||||
|
mvnw.cmd compile -DskipTests
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **BUILD SUCCESS** — все изменения компилируются без ошибок!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Автор:** AI Assistant
|
||||||
|
**Дата:** 11.04.2026
|
||||||
|
**Версия:** 2.0
|
||||||
+6
-6
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"type": "service_account",
|
"type": "service_account",
|
||||||
"project_id": "data-totality-483616-t5",
|
"project_id": "gen-lang-client-0719272404",
|
||||||
"private_key_id": "e9d6df08cfaf391f2b45ccf0473d22221a14fe1e",
|
"private_key_id": "feb98d8fa2bd9c7139b3c03fac63900877b652f0",
|
||||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkA41lMjU7GSLj\nS5CeXJjT3gsKTp2/Y9adieLrjIXA8GY3Dhm+dwj1+hVF1fKB1qYI072LNku9jpuA\nGbKtDAIDXfTr0NKF097z51+X7Sprnq3lamqzAXnH8PEAdv5A97GtNBDBB6E9S40q\n6uEBfIjeaVENcrt8bnS/jgvtQeDFj/0RoFVOUfapCgFW2zkvqU549z0kZumF9zNF\nNXkKGOelmWm1430QyCWhw4P5SIxfCehz0pD17NSMlUZQvbav/ZZ286SnDzloRT+q\njXyRIXnMMuTnKWov7zqKflmfg8omxkwf/HqGIu2c0+hgmcyQhy8oXj/X9ocXLNsA\n+IclAskFAgMBAAECggEAD+ZHl5GhnGbdksg5WZ4OCK05R8NYQnyktPqayg0szYfg\neMzVTQ6lX+cMQeFcKXRhUxtnqq5Vnad3Rpih0Wmya5jOTJVJRe+Z6O4Qk8ifeKBW\nKOvDMogy7IwxvhhK9zRWf+e7X2MPIUMziP9iVu5iJvbHBIqPR0rpPBqX6GT0VmJs\n6CG1C1FtL3QFvxiZSiWwIzGb0ozZKIHBhqUxapQcUlTSqsSNguJEGeHUXkD3Eg1R\n3gQJs4JKCiQnsCeWcpEcoo/GaP+feTjXDJzPlx6EDzpaFAeyUXPWNm8s8VBHj/dM\nvD9FSL2/YiouublF7TW+cvCCfiYALLJx41hOeeEZTwKBgQDhtbdY1FQwh6T8ciF2\nCH+kWfIUIIiNUwlyllc05Vx6nC9oSuChDOWFMqe2TOpcibAvPCWgLN4lHjxsd0zj\nInTomGFg8mPu7l9g0AUCt6R2DoWWnS2omtyRTCIiq0Ms8WHUwil3U57IS7+2MgK7\nlKwBQtrBQoWZPkgwiuxypLf/uwKBgQC6BkP3B2u2LbP5v0VlNYI1Lvr96Bk6sWCh\ny1kOO0DI/6ubC5WFgTDtTBakbZvRdk5X7OmJCaMRtNCgrAK1twq8dLT5EZFNRrQ/\n+Q/mGsMBnZlf15hqUVnjmYuXy+o9hnU1F84TOjz0AYwvZFmoWicYWf6NDoHfnXyw\nMQOlQMHuPwKBgQC4wBFPuJ0hDCbULGePQe2ZDbbFGswrQejdzombVpKYGfgEGiY8\nLV5B5PC6m8LJ9Dour8Dsl01OfK1g61YYJUAkk3ZB8KY0ERIpIJy5WLBZohFe81FI\nTVp15Cec0SqvQvWdPUDL17qEoNxPuifaexs//QUqY9UPVpeohE5+mK2zuQKBgD8S\nGMEMlwxxem/W1eR2u+rkPt0PcF5h/W1d0Jfupwx/AuPSjaNyJfCOt66QKyBdiDpv\nbM61ocBDGekf6NtLcCxtNV6tAiK9MifufeF6eVt236WLJ3YElwF/8cBJtuWdTOfv\nZkNGn5vrWmhHEc7uogqzmIaiwI3ci4YBWEcTKa1pAoGAQAGOKD6/9m15rGoypQK0\ndM7RHoFiPAga0l6RaRmjInhkPItood84nWvSGwmfDMovvLuMXT4/kqhgoAJBCTp4\nZMWlEnk5XgU3XVATV9fWogSEdPiXq+mwikl2A7xF/PsS79ekc6wVAFS6aog08GH+\ndwLlquOtv2Dd9qW6d6Pe+4I=\n-----END PRIVATE KEY-----\n",
|
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDB5ZQNAc4C6Eu\nFA1SD1GE4IxIHuKWPE0aUPe8X1YuX1l3S94XiXzwwXHgaFi8BCJJRVBRPtfvCAM+\nQvv03kfs9KEc6UvkUlaPIgF3cjddshMJuj/7W+YpEKjuuWc649l5NO4uPcug+g1S\nZJhd7UGXbx1Uglw3LSsODJ2b4wNIQUuulGym4VCLQKhu41eGElyIVbecRxrDC2N4\nR3ROgFYilAL7QYzVojTEYea3zIBAXncXvgJcFyhSWDuJq5yJbUkbPNbVNhQ7uw7G\nCFufQWMhMBeBrO62kyufQg+xI+xqMvaDOSs+JReni57ZtUqxnpjD7Gh+BxHnduJJ\n8y/osHB5AgMBAAECggEADvuWe99fXZohHgva0R7Npa5lgIXk5ONJjhfBzHYrhgD7\nu8s3iuncUBegYrvJc1pC8wuQ5cj06E9ehmo5doCcGHNbrD+s/aOotDLJ144N/FP5\nm/Hzx+Dxv95giAR0zoSiGKptEN7c9ac4HkgNn4uiUhqub2dbY77D07663fa08hg1\ntGAtqt7lbw8MZbrCrMDdqkM552xmjxn0MnS8Itkiby1MdFi4H+0xQUO7xavsOLZ6\nQXHexNgUZsjD2ZfxFoo/U/VUaWUHn1/acQh0oJyMJD9/ZerR5Cq7XNKJgUrJUn7B\ndGNaXzymOyIWHbaCkF08+KBX2th65AjSEoJgTZiYnQKBgQDqJmzik/ZomqxySUoh\nRt0WNxO6Ed775qgluQOW9TumYDRLUP4cpRqt1vA/voGpyc7C+bnQRnQnJM0gceH0\nW6AmqdmJVLX+1DJnfkVNichI30Iz5rTYzMqneirEi2kuH6Zdh7VI6jqt3fM6QB0b\nHj2ZOXT70pS3eIGMSLjQgiBZ7QKBgQDVOp6LLWSKguqaJeVCBGbkdOGgqWUb2R6/\nk2nRJCuxnK9JJICotfxTQb+MIbFZlvQGiyb6AKUQadOHrt0VdkDeP0VCTeXw88hf\npKn3fepzxUwP8qsdEg9rmI5Y0jL23oJHPT6EHf3jnUQtpR4Wy1u+c46MRiSpCqPS\nyoHIl0qvPQKBgH2vBn9QgNGBRNNLt7jI+t9zzOjxsslv9A3bk6uyEsSpmyhaXywt\nC3cIiokMzzi+FHff5/d9+dcRD/I8btblX7+hKB1pFQ+mAzA+PSDztkg3XqfRTH2a\n36SqFM7PLjQN1RUbRRnQcJLnMimwRG9L+jCHAN4zGJSq4I7Wci2py4e5AoGAbDNa\n0HUwV7knIyGnvGBPdg55j/VlgUQj/kLnp2Ebav1/VTZYs0PlzrnGYW2XeUbIfamP\nFb3OBQ4ucOOi/IkNUbn5gzPUQvjx7Na6CQS0PBsaBd22RwIrIQZA9YF0qyEuKshB\nOPmfJClht2faZOa8n2A0LapKYB0uFID+arObCmECgYEAx2PS/d178Nfi4oG6nYIR\nAHpjC3JPlt62ySSxqfag+DPdTeOyOK8YglsyLWHGU6vI2OhYCEhuU9LMawj4tVui\nS+4D3A1CS69iOMdJVBkG1jlTP7UMZHlh43jMRW+Tcu/h7fHJaSS9YWPwyX0KUAqo\n7dB0niDoH6m8Cvb9YKXCEzs=\n-----END PRIVATE KEY-----\n",
|
||||||
"client_email": "vertex-ai-user@data-totality-483616-t5.iam.gserviceaccount.com",
|
"client_email": "vertex-express@gen-lang-client-0719272404.iam.gserviceaccount.com",
|
||||||
"client_id": "116751741947593459625",
|
"client_id": "114082700099175512857",
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-user%40data-totality-483616-t5.iam.gserviceaccount.com",
|
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-express%40gen-lang-client-0719272404.iam.gserviceaccount.com",
|
||||||
"universe_domain": "googleapis.com"
|
"universe_domain": "googleapis.com"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,10 @@ public class TargetingAIAnalysisResult {
|
|||||||
private String aiRecommendationsRationale;
|
private String aiRecommendationsRationale;
|
||||||
private List<String> quickWins;
|
private List<String> quickWins;
|
||||||
private List<String> warnings;
|
private List<String> warnings;
|
||||||
|
private String technicalHealthSummary;
|
||||||
|
private Integer lpMatchScore;
|
||||||
|
private String policyRiskLevel;
|
||||||
|
private Integer suggestedAutomationRulesCount;
|
||||||
|
private Integer matrixCellsCount;
|
||||||
private TargetingRecommendationDto targetingRecommendation;
|
private TargetingRecommendationDto targetingRecommendation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class FunnelStrategyDto {
|
||||||
|
|
||||||
|
private String tof;
|
||||||
|
private String mof;
|
||||||
|
private String bof;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class HypothesisCellDto {
|
||||||
|
|
||||||
|
private int hookIndex;
|
||||||
|
private int visualIndex;
|
||||||
|
private String specificAdIdea;
|
||||||
|
private String counterHypothesis;
|
||||||
|
private String funnelStage;
|
||||||
|
private String expectedOutcome;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class HypothesisMatrixDto {
|
||||||
|
|
||||||
|
private int rowCount;
|
||||||
|
private int colCount;
|
||||||
|
private List<String> rows;
|
||||||
|
private List<String> cols;
|
||||||
|
private List<HypothesisCellDto> matrix;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class LandingPageAuditDto {
|
||||||
|
|
||||||
|
private int matchScore;
|
||||||
|
private String executiveSummary;
|
||||||
|
private List<String> criticalFixes;
|
||||||
|
private List<String> trustBoosters;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class PixelCheckDto {
|
||||||
|
|
||||||
|
private boolean fbp;
|
||||||
|
private boolean capi;
|
||||||
|
private boolean gtm;
|
||||||
|
private String setupInstructions;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class RiskAnalysisDto {
|
||||||
|
|
||||||
|
private String fatigueWarning;
|
||||||
|
private String policyRiskLevel;
|
||||||
|
private List<String> moderationTriggers;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class ScalingPlanDto {
|
||||||
|
|
||||||
|
private String horizontalScaling;
|
||||||
|
private String budgetScaling;
|
||||||
|
private List<String> geoExpansion;
|
||||||
|
}
|
||||||
@@ -15,69 +15,50 @@ import java.util.List;
|
|||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public class TargetingRecommendationDto {
|
public class TargetingRecommendationDto {
|
||||||
|
|
||||||
// ── Тип таргетинга ────────────────────────────────────────────────────────
|
|
||||||
/**
|
|
||||||
* Рекомендуемый тип: COLD_INTEREST / BEHAVIORAL / LOOKALIKE /
|
|
||||||
* RETARGETING / LEAD_GEN / AWARENESS / MIXED
|
|
||||||
*/
|
|
||||||
private String recommendedType;
|
private String recommendedType;
|
||||||
|
|
||||||
/** Читаемое название типа */
|
|
||||||
private String recommendedTypeLabel;
|
private String recommendedTypeLabel;
|
||||||
|
|
||||||
/** Почему AI выбрал именно этот тип — на основе данных опросника */
|
|
||||||
private String typeRationale;
|
private String typeRationale;
|
||||||
|
|
||||||
// ── Цель кампании ─────────────────────────────────────────────────────────
|
|
||||||
/** AWARENESS / TRAFFIC / LEADS / ENGAGEMENT / SALES */
|
|
||||||
private String campaignObjective;
|
private String campaignObjective;
|
||||||
|
|
||||||
/** Читаемая цель */
|
|
||||||
private String campaignObjectiveLabel;
|
private String campaignObjectiveLabel;
|
||||||
|
|
||||||
/** Обоснование цели */
|
|
||||||
private String campaignObjectiveRationale;
|
private String campaignObjectiveRationale;
|
||||||
|
|
||||||
// ── Аудитория ─────────────────────────────────────────────────────────────
|
|
||||||
private AudienceProfileDto audienceProfile;
|
private AudienceProfileDto audienceProfile;
|
||||||
|
|
||||||
// ── Платформы и форматы ───────────────────────────────────────────────────
|
|
||||||
private List<String> recommendedPlatforms;
|
private List<String> recommendedPlatforms;
|
||||||
private List<String> recommendedAdFormats;
|
private List<String> recommendedAdFormats;
|
||||||
private List<String> recommendedPlacements;
|
private List<String> recommendedPlacements;
|
||||||
|
|
||||||
// ── Бюджет ────────────────────────────────────────────────────────────────
|
|
||||||
private TargetingBudgetDto budgetRecommendation;
|
private TargetingBudgetDto budgetRecommendation;
|
||||||
|
|
||||||
// ── Фазы таргетинга ───────────────────────────────────────────────────────
|
|
||||||
private List<TargetingPhaseDto> phases;
|
private List<TargetingPhaseDto> phases;
|
||||||
|
|
||||||
// ── KPI ───────────────────────────────────────────────────────────────────
|
|
||||||
private String estimatedCtr;
|
private String estimatedCtr;
|
||||||
private String estimatedCpl;
|
private String estimatedCpl;
|
||||||
private String estimatedReach;
|
private String estimatedReach;
|
||||||
private String estimatedFrequency;
|
private String estimatedFrequency;
|
||||||
private String estimatedConversions;
|
private String estimatedConversions;
|
||||||
|
|
||||||
// ── Ключевые выводы из опросника ──────────────────────────────────────────
|
|
||||||
/** Сигналы из опросника, которые повлияли на рекомендацию */
|
|
||||||
private List<String> questionnaireSignals;
|
private List<String> questionnaireSignals;
|
||||||
|
|
||||||
// ── Конкурентный контекст ─────────────────────────────────────────────────
|
|
||||||
private String competitiveContext;
|
private String competitiveContext;
|
||||||
private String ciiLevel;
|
private String ciiLevel;
|
||||||
private String differentiationAdvice;
|
private String differentiationAdvice;
|
||||||
|
|
||||||
// ── Быстрые победы и предупреждения ──────────────────────────────────────
|
|
||||||
private List<String> quickWins;
|
private List<String> quickWins;
|
||||||
private List<String> warnings;
|
private List<String> warnings;
|
||||||
|
|
||||||
// ── Контент-стратегия ─────────────────────────────────────────────────────
|
private PixelCheckDto pixelHealth;
|
||||||
|
private FunnelStrategyDto funnelStrategy;
|
||||||
|
private HypothesisMatrixDto hypothesisMatrix;
|
||||||
|
private LandingPageAuditDto lpAudit;
|
||||||
|
private List<TargetingOrchestrationDto.AutomationRuleDto> suggestedRules;
|
||||||
|
private ScalingPlanDto scalingPlan;
|
||||||
|
private RiskAnalysisDto risks;
|
||||||
|
|
||||||
private List<String> contentMix;
|
private List<String> contentMix;
|
||||||
private String postingFrequency;
|
private String postingFrequency;
|
||||||
private List<String> bestPostingTimes;
|
private List<String> bestPostingTimes;
|
||||||
|
|
||||||
// ── CTA и воронка ─────────────────────────────────────────────────────────
|
|
||||||
private String primaryCta;
|
private String primaryCta;
|
||||||
private String funnelStage;
|
private String funnelStage;
|
||||||
private String conversionMechanism;
|
private String conversionMechanism;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
||||||
import kz.konturai.parser.dto.targeting.*;
|
import kz.konturai.parser.dto.targeting.*;
|
||||||
|
import kz.konturai.parser.enums.StrategyModel;
|
||||||
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||||
import kz.konturai.parser.model.MarketingStrategy;
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
||||||
@@ -15,7 +16,14 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI Targeting System — два независимых режима:
|
* AI Targeting System — два независимых режима:
|
||||||
@@ -164,23 +172,26 @@ public class AiTargetingSystemService {
|
|||||||
- Бюджеты указывай в тенге (KZT).
|
- Бюджеты указывай в тенге (KZT).
|
||||||
""";
|
""";
|
||||||
|
|
||||||
String instruction = buildTargetingPromptInstruction();
|
systemPrompt = buildEnhancedTargetingSystemPrompt();
|
||||||
String context = buildFullContext(req, res, strategy);
|
String instruction = buildEnhancedTargetingPromptInstruction();
|
||||||
|
String context = buildEnhancedFullContext(req, res, strategy);
|
||||||
|
|
||||||
String raw = openAIAnalyticsService.generateWithInstructionWithModel(
|
String raw = openAIAnalyticsService.generateWithInstructionWithModel(
|
||||||
context, instruction, "ru", "gpt-4o",
|
context, instruction, "ru", strategyModel,
|
||||||
systemPrompt, 3000, 150000L);
|
systemPrompt, 4500, 150000L);
|
||||||
|
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
logger.warn("AI returned null for targeting recommendation, using fallback");
|
logger.warn("AI returned null for targeting recommendation, using fallback");
|
||||||
return buildFallbackRecommendation(req, res);
|
return buildFallbackRecommendation(req, res, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(extractJson(raw), TargetingRecommendationDto.class);
|
TargetingRecommendationDto recommendation =
|
||||||
|
objectMapper.readValue(extractJson(raw), TargetingRecommendationDto.class);
|
||||||
|
return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to parse targeting recommendation. Raw: {}", raw, e);
|
logger.error("Failed to parse targeting recommendation", e);
|
||||||
return buildFallbackRecommendation(req, res);
|
return buildFallbackRecommendation(req, res, strategy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,6 +474,619 @@ public class AiTargetingSystemService {
|
|||||||
return ctx.toString();
|
return ctx.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildEnhancedTargetingSystemPrompt() {
|
||||||
|
return """
|
||||||
|
You are NOT just a generator. You are a Senior Performance Architect with 20 years of experience in paid acquisition,
|
||||||
|
funnel strategy, landing page diagnostics, Meta Ads scaling, and growth systems.
|
||||||
|
Your recommendations must be surgical, commercially sharp, aggressive for growth, but cautious about technical health
|
||||||
|
and burn-out risk. You speak like a real performance lead and use concepts such as LTV, ROAS, CPL, CAC,
|
||||||
|
Attribution Window, Retargeting windows, budget pacing, creative fatigue, and conversion friction.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Return valid JSON only.
|
||||||
|
- Every narrative field inside JSON must be in Russian.
|
||||||
|
- Keep recommendations practical and compatible with real paid traffic operations.
|
||||||
|
- Budget values must stay in KZT.
|
||||||
|
- Do not omit mandatory sections even if data is limited.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildEnhancedTargetingPromptInstruction() {
|
||||||
|
return """
|
||||||
|
На основе всех данных опросника, анализа рынка, текущих digital assets, сильных сторон конкурентов и модели стратегии
|
||||||
|
верни только валидный JSON без markdown и без пояснений вне JSON.
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- Все текстовые поля на русском языке.
|
||||||
|
- Используй профессиональную performance-терминологию: ROAS, CPL, CTR, LTV, CAC, Pixel, CAPI, GTM, Retargeting.
|
||||||
|
- Не пропускай ни один обязательный блок.
|
||||||
|
- hypothesisMatrix обязан содержать ровно 4 rows, 4 cols и 16 cells.
|
||||||
|
- lpAudit.matchScore должен быть числом от 0 до 100.
|
||||||
|
- suggestedRules должен содержать 3 реалистичных automation rules.
|
||||||
|
- Funnel strategy обязана разделять холодную, теплую и горячую аудиторию.
|
||||||
|
- LP audit должен указывать 3 типичных conversion killers ниши.
|
||||||
|
- Risk analysis обязан учитывать policy risk и creative fatigue.
|
||||||
|
|
||||||
|
Верни JSON со всеми существующими полями recommendation и обязательными новыми блоками:
|
||||||
|
pixelHealth, funnelStrategy, hypothesisMatrix, lpAudit, suggestedRules, scalingPlan, risks.
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildEnhancedFullContext(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
StringBuilder ctx = new StringBuilder(buildFullContext(req, res, strategy));
|
||||||
|
appendSocialLinksContext(ctx, req);
|
||||||
|
appendCompetitorStrengthsContext(ctx, res);
|
||||||
|
appendCompetitiveInterpretation(ctx, req, res);
|
||||||
|
appendStrategyModelContext(ctx, strategy);
|
||||||
|
return ctx.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendSocialLinksContext(StringBuilder ctx, MarketingAnalysisV3Request req) {
|
||||||
|
if (req == null || req.getUserSocialLinks() == null || req.getUserSocialLinks().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.append("\n=== CURRENT DIGITAL ASSETS ===\n");
|
||||||
|
req.getUserSocialLinks().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(link -> !link.isBlank())
|
||||||
|
.forEach(link -> ctx.append("- ").append(link).append("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendCompetitorStrengthsContext(StringBuilder ctx, MarketingAnalysisV3Result res) {
|
||||||
|
List<String> strengths = extractTopCompetitorStrengths(res);
|
||||||
|
if (strengths.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.append("\n=== TOP COMPETITOR STRENGTHS ===\n");
|
||||||
|
strengths.forEach(strength -> ctx.append("- ").append(strength).append("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> extractTopCompetitorStrengths(MarketingAnalysisV3Result res) {
|
||||||
|
if (res == null || res.getCompetitorMap() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return res.getCompetitorMap().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.sorted(Comparator
|
||||||
|
.comparingInt((MarketingAnalysisV3Result.CompetitorProfile profile) ->
|
||||||
|
profile.getStrengths() != null ? profile.getStrengths().size() : 0)
|
||||||
|
.reversed()
|
||||||
|
.thenComparingInt(MarketingAnalysisV3Result.CompetitorProfile::getFollowers).reversed())
|
||||||
|
.limit(3)
|
||||||
|
.flatMap(profile -> {
|
||||||
|
List<String> strengths = profile.getStrengths();
|
||||||
|
if (strengths == null || strengths.isEmpty()) {
|
||||||
|
return List.<String>of().stream();
|
||||||
|
}
|
||||||
|
return strengths.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.map(s -> safeStr(profile.getName()) + ": " + s);
|
||||||
|
})
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendCompetitiveInterpretation(
|
||||||
|
StringBuilder ctx,
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res) {
|
||||||
|
if (res == null || res.getCompetitionIntensity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int cii = res.getCompetitionIntensity().getCiiIndex();
|
||||||
|
ctx.append("\n=== PERFORMANCE INTERPRETATION ===\n");
|
||||||
|
if (cii >= 70) {
|
||||||
|
ctx.append("- Нужна агрессивная differentiation strategy и жесткий контроль creative fatigue.\n");
|
||||||
|
} else if (cii >= 40) {
|
||||||
|
ctx.append("- Нужен баланс между дифференциацией и cost-efficient закупкой.\n");
|
||||||
|
} else {
|
||||||
|
ctx.append("- Можно активнее тестировать холодный трафик и более широкий охват.\n");
|
||||||
|
}
|
||||||
|
if (req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name())) {
|
||||||
|
ctx.append("- Высокий чек: усиливаем trust layer, BOF и качественную аргументацию оффера.\n");
|
||||||
|
}
|
||||||
|
if (res.getContentProfile() != null) {
|
||||||
|
ctx.append("- Доля video content у конкурентов: ")
|
||||||
|
.append(res.getContentProfile().getVideoShare())
|
||||||
|
.append("%.\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendStrategyModelContext(StringBuilder ctx, MarketingStrategy strategy) {
|
||||||
|
if (strategy == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.append("\n=== STRATEGY MODEL CONTEXT ===\n");
|
||||||
|
ctx.append("- scoringModelName: ").append(safeStr(strategy.getScoringModelName())).append("\n");
|
||||||
|
ctx.append("- scoringModelTitle: ").append(safeStr(strategy.getScoringModelTitle())).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingRecommendationDto validateAndNormalizeRecommendation(
|
||||||
|
TargetingRecommendationDto recommendation,
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
TargetingRecommendationDto base = buildFallbackRecommendation(req, res);
|
||||||
|
TargetingRecommendationDto normalized = recommendation != null ? recommendation : base;
|
||||||
|
|
||||||
|
if (isBlank(normalized.getRecommendedType())) normalized.setRecommendedType(base.getRecommendedType());
|
||||||
|
if (isBlank(normalized.getRecommendedTypeLabel())) normalized.setRecommendedTypeLabel(base.getRecommendedTypeLabel());
|
||||||
|
if (isBlank(normalized.getTypeRationale())) normalized.setTypeRationale(base.getTypeRationale());
|
||||||
|
if (isBlank(normalized.getCampaignObjective())) normalized.setCampaignObjective(base.getCampaignObjective());
|
||||||
|
if (isBlank(normalized.getCampaignObjectiveLabel())) normalized.setCampaignObjectiveLabel(base.getCampaignObjectiveLabel());
|
||||||
|
if (isBlank(normalized.getCampaignObjectiveRationale())) normalized.setCampaignObjectiveRationale(base.getCampaignObjectiveRationale());
|
||||||
|
if (normalized.getAudienceProfile() == null) normalized.setAudienceProfile(base.getAudienceProfile());
|
||||||
|
if (normalized.getBudgetRecommendation() == null) normalized.setBudgetRecommendation(base.getBudgetRecommendation());
|
||||||
|
if (normalized.getRecommendedPlatforms() == null || normalized.getRecommendedPlatforms().isEmpty()) {
|
||||||
|
normalized.setRecommendedPlatforms(base.getRecommendedPlatforms());
|
||||||
|
}
|
||||||
|
if (normalized.getRecommendedAdFormats() == null || normalized.getRecommendedAdFormats().isEmpty()) {
|
||||||
|
normalized.setRecommendedAdFormats(base.getRecommendedAdFormats());
|
||||||
|
}
|
||||||
|
if (normalized.getQuickWins() == null || normalized.getQuickWins().isEmpty()) {
|
||||||
|
normalized.setQuickWins(base.getQuickWins());
|
||||||
|
}
|
||||||
|
if (normalized.getWarnings() == null || normalized.getWarnings().isEmpty()) {
|
||||||
|
normalized.setWarnings(base.getWarnings());
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.setQuestionnaireSignals(normalizeTextList(
|
||||||
|
normalized.getQuestionnaireSignals(),
|
||||||
|
List.of(
|
||||||
|
"Покупатель принимает решение не мгновенно и требует прогрева.",
|
||||||
|
"Сегменту важна понятная ценность оффера до клика.",
|
||||||
|
"Конкурентная среда влияет на стоимость лида и частоту тестов.",
|
||||||
|
"Таргетинг должен опираться на реальный customer behavior, а не на абстрактные интересы."
|
||||||
|
),
|
||||||
|
4));
|
||||||
|
|
||||||
|
normalized.setRecommendedPlacements(normalizeTextList(
|
||||||
|
normalized.getRecommendedPlacements(),
|
||||||
|
List.of("Instagram Feed", "Instagram Stories", "Facebook Feed"),
|
||||||
|
3));
|
||||||
|
|
||||||
|
if (normalized.getPixelHealth() == null) {
|
||||||
|
normalized.setPixelHealth(buildDefaultPixelHealth(req, res, strategy));
|
||||||
|
} else {
|
||||||
|
normalized.getPixelHealth().setSetupInstructions(truncateText(
|
||||||
|
defaultIfBlank(normalized.getPixelHealth().getSetupInstructions(),
|
||||||
|
buildDefaultPixelHealth(req, res, strategy).getSetupInstructions()),
|
||||||
|
600));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.getFunnelStrategy() == null) {
|
||||||
|
normalized.setFunnelStrategy(buildDefaultFunnelStrategy(req, res, strategy));
|
||||||
|
} else {
|
||||||
|
FunnelStrategyDto fallbackFunnel = buildDefaultFunnelStrategy(req, res, strategy);
|
||||||
|
normalized.getFunnelStrategy().setTof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getTof(), fallbackFunnel.getTof()), 450));
|
||||||
|
normalized.getFunnelStrategy().setMof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getMof(), fallbackFunnel.getMof()), 450));
|
||||||
|
normalized.getFunnelStrategy().setBof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getBof(), fallbackFunnel.getBof()), 450));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.setHypothesisMatrix(normalizeMatrix(normalized.getHypothesisMatrix(), req, res, strategy));
|
||||||
|
|
||||||
|
if (normalized.getLpAudit() == null) {
|
||||||
|
normalized.setLpAudit(buildDefaultLandingPageAudit(req, res, strategy));
|
||||||
|
} else {
|
||||||
|
LandingPageAuditDto fallbackAudit = buildDefaultLandingPageAudit(req, res, strategy);
|
||||||
|
normalized.getLpAudit().setMatchScore(Math.max(0, Math.min(100, normalized.getLpAudit().getMatchScore())));
|
||||||
|
normalized.getLpAudit().setExecutiveSummary(truncateText(
|
||||||
|
defaultIfBlank(normalized.getLpAudit().getExecutiveSummary(), fallbackAudit.getExecutiveSummary()),
|
||||||
|
500));
|
||||||
|
normalized.getLpAudit().setCriticalFixes(normalizeTextList(
|
||||||
|
normalized.getLpAudit().getCriticalFixes(),
|
||||||
|
fallbackAudit.getCriticalFixes(),
|
||||||
|
3));
|
||||||
|
normalized.getLpAudit().setTrustBoosters(normalizeTextList(
|
||||||
|
normalized.getLpAudit().getTrustBoosters(),
|
||||||
|
fallbackAudit.getTrustBoosters(),
|
||||||
|
3));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.setSuggestedRules(normalizeRules(normalized.getSuggestedRules(), req, res, strategy));
|
||||||
|
|
||||||
|
if (normalized.getScalingPlan() == null) {
|
||||||
|
normalized.setScalingPlan(buildDefaultScalingPlan(req, res, strategy));
|
||||||
|
} else {
|
||||||
|
ScalingPlanDto fallbackScaling = buildDefaultScalingPlan(req, res, strategy);
|
||||||
|
normalized.getScalingPlan().setHorizontalScaling(truncateText(
|
||||||
|
defaultIfBlank(normalized.getScalingPlan().getHorizontalScaling(), fallbackScaling.getHorizontalScaling()), 400));
|
||||||
|
normalized.getScalingPlan().setBudgetScaling(truncateText(
|
||||||
|
defaultIfBlank(normalized.getScalingPlan().getBudgetScaling(), fallbackScaling.getBudgetScaling()), 400));
|
||||||
|
normalized.getScalingPlan().setGeoExpansion(normalizeTextList(
|
||||||
|
normalized.getScalingPlan().getGeoExpansion(),
|
||||||
|
fallbackScaling.getGeoExpansion(),
|
||||||
|
3));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.getRisks() == null) {
|
||||||
|
normalized.setRisks(buildDefaultRiskAnalysis(req, res, strategy));
|
||||||
|
} else {
|
||||||
|
RiskAnalysisDto fallbackRisk = buildDefaultRiskAnalysis(req, res, strategy);
|
||||||
|
normalized.getRisks().setFatigueWarning(truncateText(
|
||||||
|
defaultIfBlank(normalized.getRisks().getFatigueWarning(), fallbackRisk.getFatigueWarning()), 400));
|
||||||
|
normalized.getRisks().setPolicyRiskLevel(normalizePolicyRiskLevel(
|
||||||
|
defaultIfBlank(normalized.getRisks().getPolicyRiskLevel(), fallbackRisk.getPolicyRiskLevel())));
|
||||||
|
normalized.getRisks().setModerationTriggers(normalizeTextList(
|
||||||
|
normalized.getRisks().getModerationTriggers(),
|
||||||
|
fallbackRisk.getModerationTriggers(),
|
||||||
|
3));
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.setCompetitiveContext(truncateText(defaultIfBlank(normalized.getCompetitiveContext(), "Конкурентная среда требует отдельного тестирования офферов, аудиторий и креативных углов."), 400));
|
||||||
|
normalized.setCiiLevel(truncateText(defaultIfBlank(normalized.getCiiLevel(), buildDefaultCiiLevel(res)), 200));
|
||||||
|
normalized.setDifferentiationAdvice(truncateText(defaultIfBlank(normalized.getDifferentiationAdvice(), "Нужен отдельный message-market fit в креативах и посадочной странице, чтобы не конкурировать только ценой."), 400));
|
||||||
|
normalized.setPrimaryCta(truncateText(defaultIfBlank(normalized.getPrimaryCta(), "Записаться на консультацию"), 120));
|
||||||
|
normalized.setFunnelStage(truncateText(defaultIfBlank(normalized.getFunnelStage(), "Середина воронки - прогрев и квалификация лидов"), 200));
|
||||||
|
normalized.setConversionMechanism(truncateText(defaultIfBlank(normalized.getConversionMechanism(), "Lead Ads -> менеджер -> квалификация -> продажа"), 220));
|
||||||
|
normalized.setContentMix(normalizeTextList(
|
||||||
|
normalized.getContentMix(),
|
||||||
|
List.of("40% - демонстрационный контент", "30% - социальные доказательства", "20% - экспертный контент", "10% - продажи"),
|
||||||
|
4));
|
||||||
|
normalized.setBestPostingTimes(normalizeTextList(
|
||||||
|
normalized.getBestPostingTimes(),
|
||||||
|
List.of("09:00", "13:00", "19:00 - 21:00"),
|
||||||
|
3));
|
||||||
|
normalized.setPostingFrequency(defaultIfBlank(normalized.getPostingFrequency(), "1 раз в день в рабочие дни, 2 раза в выходные"));
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HypothesisMatrixDto normalizeMatrix(
|
||||||
|
HypothesisMatrixDto matrix,
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
HypothesisMatrixDto fallback = buildDefaultStrategicMatrix(req, res, strategy);
|
||||||
|
if (matrix == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> rows = normalizeTextList(matrix.getRows(), fallback.getRows(), 4);
|
||||||
|
List<String> cols = normalizeTextList(matrix.getCols(), fallback.getCols(), 4);
|
||||||
|
List<HypothesisCellDto> fallbackCells = fallback.getMatrix();
|
||||||
|
List<HypothesisCellDto> incoming = matrix.getMatrix() != null ? matrix.getMatrix() : List.of();
|
||||||
|
List<HypothesisCellDto> normalizedCells = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int row = 0; row < 4; row++) {
|
||||||
|
for (int col = 0; col < 4; col++) {
|
||||||
|
final int currentRow = row;
|
||||||
|
final int currentCol = col;
|
||||||
|
HypothesisCellDto source = incoming.stream()
|
||||||
|
.filter(cell -> cell != null && cell.getHookIndex() == currentRow && cell.getVisualIndex() == currentCol)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(fallbackCells.get(row * 4 + col));
|
||||||
|
|
||||||
|
normalizedCells.add(HypothesisCellDto.builder()
|
||||||
|
.hookIndex(row)
|
||||||
|
.visualIndex(col)
|
||||||
|
.specificAdIdea(truncateText(defaultIfBlank(source.getSpecificAdIdea(),
|
||||||
|
fallbackCells.get(row * 4 + col).getSpecificAdIdea()), 240))
|
||||||
|
.counterHypothesis(truncateText(defaultIfBlank(source.getCounterHypothesis(),
|
||||||
|
fallbackCells.get(row * 4 + col).getCounterHypothesis()), 220))
|
||||||
|
.funnelStage(normalizeFunnelStage(defaultIfBlank(source.getFunnelStage(),
|
||||||
|
fallbackCells.get(row * 4 + col).getFunnelStage())))
|
||||||
|
.expectedOutcome(truncateText(defaultIfBlank(source.getExpectedOutcome(),
|
||||||
|
fallbackCells.get(row * 4 + col).getExpectedOutcome()), 220))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HypothesisMatrixDto.builder()
|
||||||
|
.rowCount(4)
|
||||||
|
.colCount(4)
|
||||||
|
.rows(rows)
|
||||||
|
.cols(cols)
|
||||||
|
.matrix(normalizedCells)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TargetingOrchestrationDto.AutomationRuleDto> normalizeRules(
|
||||||
|
List<TargetingOrchestrationDto.AutomationRuleDto> rules,
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
List<TargetingOrchestrationDto.AutomationRuleDto> fallback = buildDefaultAutomationRules(req, res, strategy);
|
||||||
|
if (rules == null || rules.isEmpty()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
List<TargetingOrchestrationDto.AutomationRuleDto> normalized = new ArrayList<>();
|
||||||
|
for (TargetingOrchestrationDto.AutomationRuleDto rule : rules) {
|
||||||
|
if (rule == null || rule.getConditions() == null || rule.getConditions().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.add(TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||||
|
.ruleId(defaultIfBlank(rule.getRuleId(), "rule-" + (normalized.size() + 1)))
|
||||||
|
.entityLevel(defaultIfBlank(rule.getEntityLevel(), "ADSET").toUpperCase(Locale.ROOT))
|
||||||
|
.conditionGroup(rule.getConditionGroup() != null ? rule.getConditionGroup() : TargetingOrchestrationDto.ConditionGroup.AND)
|
||||||
|
.conditions(rule.getConditions().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(condition -> TargetingOrchestrationDto.ConditionDto.builder()
|
||||||
|
.metric(defaultIfBlank(condition.getMetric(), "spend").toLowerCase(Locale.ROOT))
|
||||||
|
.operator(condition.getOperator() != null ? condition.getOperator() : TargetingOrchestrationDto.Operator.GREATER_THAN)
|
||||||
|
.value(condition.getValue() != null ? condition.getValue() : BigDecimal.ZERO)
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.action(defaultIfBlank(rule.getAction(), "PAUSE").toUpperCase(Locale.ROOT))
|
||||||
|
.actionValue(rule.getActionValue() != null ? rule.getActionValue() : BigDecimal.ZERO)
|
||||||
|
.build());
|
||||||
|
if (normalized.size() == 3) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (normalized.size() < 3) {
|
||||||
|
normalized.add(fallback.get(normalized.size()));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingRecommendationDto buildFallbackRecommendation(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
TargetingRecommendationDto recommendation = buildFallbackRecommendation(req, res);
|
||||||
|
return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PixelCheckDto buildDefaultPixelHealth(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
boolean capi = req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name());
|
||||||
|
boolean gtm = req != null && req.getUserSocialLinks() != null && !req.getUserSocialLinks().isEmpty();
|
||||||
|
return PixelCheckDto.builder()
|
||||||
|
.fbp(true)
|
||||||
|
.capi(capi)
|
||||||
|
.gtm(gtm)
|
||||||
|
.setupInstructions("Настройте Pixel события PageView, ViewContent и Lead, передайте fbp/fbc, подключите GTM для контроля событий и включите CAPI там, где важна стабильная атрибуция.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private FunnelStrategyDto buildDefaultFunnelStrategy(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
|
||||||
|
return FunnelStrategyDto.builder()
|
||||||
|
.tof("На холодную аудиторию показываем понятную ценность " + niche + ", демонстрацию результата и мягкий CTA без давления на покупку.")
|
||||||
|
.mof("Для Retargeting по 50%+ видео, вовлеченным и посетителям лендинга усиливаем кейсы, FAQ, social proof и снятие возражений.")
|
||||||
|
.bof("Горячим лидам даем hard-sell оффер: дедлайн, бонус, ограничение по местам и быстрый путь до менеджера или формы.")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HypothesisMatrixDto buildDefaultStrategicMatrix(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
StrategyModel model = resolveStrategyModel(strategy);
|
||||||
|
List<String> rows = switch (model) {
|
||||||
|
case AUTHORITY -> List.of("Экспертный разбор проблемы", "Кейсы с цифрами и исходом", "Методология и процесс работы", "Результат и измеримый эффект");
|
||||||
|
case TRUST -> List.of("Отзывы и подтверждения", "Гарантии и прозрачность", "Реальный процесс без прикрас", "Доказательства качества");
|
||||||
|
case CONVERSION -> List.of("Дедлайн и ограничение", "Выгода и конкретный оффер", "Прямой призыв к действию", "Сравнение потерь и выгоды");
|
||||||
|
case ENTRY -> List.of("Простой первый шаг", "Доступность и понятный старт", "Быстрый результат без сложности", "Минимальный барьер входа");
|
||||||
|
};
|
||||||
|
List<String> cols = List.of("UGC-style видео", "Карточка с оффером", "Скриншот или proof-format", "До/после или демонстрация");
|
||||||
|
List<String> competitorStrengths = extractTopCompetitorStrengths(res);
|
||||||
|
List<HypothesisCellDto> cells = new ArrayList<>();
|
||||||
|
for (int row = 0; row < rows.size(); row++) {
|
||||||
|
for (int col = 0; col < cols.size(); col++) {
|
||||||
|
String stage = row < 2 ? "TOF" : row == 2 ? "MOF" : "BOF";
|
||||||
|
String counter = competitorStrengths.isEmpty()
|
||||||
|
? "Контр-гипотеза: усиливаем дифференциацию оффера и message-market fit."
|
||||||
|
: "Контр-гипотеза: переиграть сильную сторону конкурента \"" + competitorStrengths.get((row + col) % competitorStrengths.size()) + "\" через более конкретный value proposition.";
|
||||||
|
cells.add(HypothesisCellDto.builder()
|
||||||
|
.hookIndex(row)
|
||||||
|
.visualIndex(col)
|
||||||
|
.specificAdIdea(rows.get(row) + " + " + cols.get(col) + ": показать реальный сценарий использования, оффер и следующий шаг до заявки.")
|
||||||
|
.counterHypothesis(counter)
|
||||||
|
.funnelStage(stage)
|
||||||
|
.expectedOutcome("Проверить CTR, вовлечение и влияние на CPL на стадии " + stage + ".")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HypothesisMatrixDto.builder()
|
||||||
|
.rowCount(4)
|
||||||
|
.colCount(4)
|
||||||
|
.rows(rows)
|
||||||
|
.cols(cols)
|
||||||
|
.matrix(cells)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LandingPageAuditDto buildDefaultLandingPageAudit(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
|
||||||
|
return LandingPageAuditDto.builder()
|
||||||
|
.matchScore(72)
|
||||||
|
.executiveSummary("Лендинг для " + niche + " должен быстрее раскрывать оффер, social proof и путь до конверсии. Сейчас критична ясность первого экрана и связка с рекламным сообщением.")
|
||||||
|
.criticalFixes(List.of(
|
||||||
|
"Упростить первый экран: один оффер, один CTA, без перегруза текстом.",
|
||||||
|
"Добавить кейсы, отзывы или доказательства результата выше первого сильного скролла.",
|
||||||
|
"Сократить путь до заявки: меньше полей, понятная форма или быстрый мессенджер-канал."
|
||||||
|
))
|
||||||
|
.trustBoosters(List.of(
|
||||||
|
"Показать реальные отзывы, кейсы или цифры результата с конкретикой.",
|
||||||
|
"Добавить прозрачные условия: сроки, стоимость, что входит в оффер.",
|
||||||
|
"Подсветить экспертность команды, реальные лица и подтверждения качества."
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TargetingOrchestrationDto.AutomationRuleDto> buildDefaultAutomationRules(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
return List.of(
|
||||||
|
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||||
|
.ruleId("rule-kill-zero-leads")
|
||||||
|
.entityLevel("ADSET")
|
||||||
|
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||||
|
.conditions(List.of(
|
||||||
|
buildCondition("spend", TargetingOrchestrationDto.Operator.GREATER_THAN, 6000),
|
||||||
|
buildCondition("leads", TargetingOrchestrationDto.Operator.EQUAL, 0)
|
||||||
|
))
|
||||||
|
.action("PAUSE")
|
||||||
|
.actionValue(BigDecimal.ZERO)
|
||||||
|
.build(),
|
||||||
|
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||||
|
.ruleId("rule-scale-winning-adset")
|
||||||
|
.entityLevel("ADSET")
|
||||||
|
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||||
|
.conditions(List.of(
|
||||||
|
buildCondition("roas", TargetingOrchestrationDto.Operator.GREATER_THAN, 4),
|
||||||
|
buildCondition("purchases", TargetingOrchestrationDto.Operator.GREATER_THAN_OR_EQUAL, 3)
|
||||||
|
))
|
||||||
|
.action("INCREASE_BUDGET")
|
||||||
|
.actionValue(BigDecimal.valueOf(20))
|
||||||
|
.build(),
|
||||||
|
TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||||
|
.ruleId("rule-stop-fatigue")
|
||||||
|
.entityLevel("AD")
|
||||||
|
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||||
|
.conditions(List.of(
|
||||||
|
buildCondition("ctr", TargetingOrchestrationDto.Operator.LESS_THAN, 1),
|
||||||
|
buildCondition("frequency", TargetingOrchestrationDto.Operator.GREATER_THAN, 3)
|
||||||
|
))
|
||||||
|
.action("PAUSE")
|
||||||
|
.actionValue(BigDecimal.ZERO)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScalingPlanDto buildDefaultScalingPlan(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
List<String> cities = req != null && req.getPromotionCities() != null && !req.getPromotionCities().isEmpty()
|
||||||
|
? normalizeTextList(req.getPromotionCities(), List.of("Алматы", "Астана", "Шымкент"), 3)
|
||||||
|
: List.of("Алматы", "Астана", "Шымкент");
|
||||||
|
return ScalingPlanDto.builder()
|
||||||
|
.horizontalScaling("Масштабируйте через новые lookalike/interest-кластеры, отдельные creative angles и раздельные placements для Reels, Stories и Feed.")
|
||||||
|
.budgetScaling("Повышайте бюджет ступенчато на 15-20% каждые 48-72 часа только на связках с устойчивым CPL/ROAS.")
|
||||||
|
.geoExpansion(cities)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RiskAnalysisDto buildDefaultRiskAnalysis(
|
||||||
|
MarketingAnalysisV3Request req,
|
||||||
|
MarketingAnalysisV3Result res,
|
||||||
|
MarketingStrategy strategy) {
|
||||||
|
int cii = res != null && res.getCompetitionIntensity() != null ? res.getCompetitionIntensity().getCiiIndex() : 50;
|
||||||
|
String level = cii >= 70
|
||||||
|
? "High - высокий риск модерации и быстрого выгорания креативов."
|
||||||
|
: cii >= 40
|
||||||
|
? "Medium - умеренный риск, нужен контроль claims и частоты."
|
||||||
|
: "Low - низкий риск, но формулировки все равно нужно проверять.";
|
||||||
|
return RiskAnalysisDto.builder()
|
||||||
|
.fatigueWarning("При текущей конкурентности креативы могут выгорать за 7-14 дней, поэтому нужно заранее готовить новую матрицу углов и визуалов.")
|
||||||
|
.policyRiskLevel(level)
|
||||||
|
.moderationTriggers(List.of(
|
||||||
|
"Категоричные обещания результата без доказательств.",
|
||||||
|
"Формулировки, давящие на страх, боль или личную уязвимость пользователя.",
|
||||||
|
"Слишком агрессивные before/after claims, медицинские или финансовые обещания."
|
||||||
|
))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingOrchestrationDto.ConditionDto buildCondition(
|
||||||
|
String metric,
|
||||||
|
TargetingOrchestrationDto.Operator operator,
|
||||||
|
double value) {
|
||||||
|
return TargetingOrchestrationDto.ConditionDto.builder()
|
||||||
|
.metric(metric)
|
||||||
|
.operator(operator)
|
||||||
|
.value(BigDecimal.valueOf(value))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private StrategyModel resolveStrategyModel(MarketingStrategy strategy) {
|
||||||
|
if (strategy == null || strategy.getScoringModelName() == null || strategy.getScoringModelName().isBlank()) {
|
||||||
|
return StrategyModel.ENTRY;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return StrategyModel.valueOf(strategy.getScoringModelName().trim().toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return StrategyModel.ENTRY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDefaultCiiLevel(MarketingAnalysisV3Result res) {
|
||||||
|
if (res == null || res.getCompetitionIntensity() == null) {
|
||||||
|
return "Средний уровень конкуренции - нужен контролируемый тест офферов и аудиторий.";
|
||||||
|
}
|
||||||
|
MarketingAnalysisV3Result.CompetitionIntensity ci = res.getCompetitionIntensity();
|
||||||
|
return safeStr(ci.getIntensityLabel()) + " (CII=" + ci.getCiiIndex() + "/100)";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeTextList(List<String> source, List<String> fallback, int minSize) {
|
||||||
|
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||||
|
if (source != null) {
|
||||||
|
source.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.map(value -> truncateText(value, 220))
|
||||||
|
.forEach(values::add);
|
||||||
|
}
|
||||||
|
if (fallback != null) {
|
||||||
|
fallback.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.forEach(values::add);
|
||||||
|
}
|
||||||
|
List<String> normalized = new ArrayList<>(values);
|
||||||
|
while (normalized.size() < minSize && fallback != null && !fallback.isEmpty()) {
|
||||||
|
normalized.add(fallback.get(normalized.size() % fallback.size()));
|
||||||
|
}
|
||||||
|
if (normalized.size() > minSize) {
|
||||||
|
return normalized.subList(0, minSize);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizePolicyRiskLevel(String value) {
|
||||||
|
String normalized = defaultIfBlank(value, "Medium - умеренный риск модерации.");
|
||||||
|
String lower = normalized.toLowerCase(Locale.ROOT);
|
||||||
|
if (lower.contains("high") || lower.contains("выс")) {
|
||||||
|
return "High - высокий риск модерации и выгорания креативов.";
|
||||||
|
}
|
||||||
|
if (lower.contains("low") || lower.contains("низ")) {
|
||||||
|
return "Low - низкий риск модерации, но формулировки все равно нужно проверять.";
|
||||||
|
}
|
||||||
|
return "Medium - умеренный риск модерации, нужен контроль claims и pressure tactics.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeFunnelStage(String value) {
|
||||||
|
String upper = defaultIfBlank(value, "TOF").toUpperCase(Locale.ROOT);
|
||||||
|
if (upper.contains("BOF")) return "BOF";
|
||||||
|
if (upper.contains("MOF")) return "MOF";
|
||||||
|
return "TOF";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultIfBlank(String value, String fallback) {
|
||||||
|
return isBlank(value) ? fallback : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBlank(String value) {
|
||||||
|
return value == null || value.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncateText(String value, int maxLen) {
|
||||||
|
if (value == null || value.length() <= maxLen) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.substring(0, maxLen - 3).trim() + "...";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Публикация первого поста ──────────────────────────────────────────────
|
// ── Публикация первого поста ──────────────────────────────────────────────
|
||||||
|
|
||||||
private PublishedPostDto publishFirstPost(MarketingStrategy strategy) {
|
private PublishedPostDto publishFirstPost(MarketingStrategy strategy) {
|
||||||
|
|||||||
@@ -549,10 +549,37 @@ public class TargetingCampaignService {
|
|||||||
.aiRecommendationsRationale(recommendation != null ? recommendation.getTypeRationale() : null)
|
.aiRecommendationsRationale(recommendation != null ? recommendation.getTypeRationale() : null)
|
||||||
.quickWins(recommendation != null ? recommendation.getQuickWins() : List.of())
|
.quickWins(recommendation != null ? recommendation.getQuickWins() : List.of())
|
||||||
.warnings(recommendation != null ? recommendation.getWarnings() : List.of())
|
.warnings(recommendation != null ? recommendation.getWarnings() : List.of())
|
||||||
|
.technicalHealthSummary(buildTechnicalHealthSummary(recommendation))
|
||||||
|
.lpMatchScore(recommendation != null && recommendation.getLpAudit() != null
|
||||||
|
? recommendation.getLpAudit().getMatchScore() : null)
|
||||||
|
.policyRiskLevel(recommendation != null && recommendation.getRisks() != null
|
||||||
|
? recommendation.getRisks().getPolicyRiskLevel() : null)
|
||||||
|
.suggestedAutomationRulesCount(recommendation != null && recommendation.getSuggestedRules() != null
|
||||||
|
? recommendation.getSuggestedRules().size() : 0)
|
||||||
|
.matrixCellsCount(recommendation != null
|
||||||
|
&& recommendation.getHypothesisMatrix() != null
|
||||||
|
&& recommendation.getHypothesisMatrix().getMatrix() != null
|
||||||
|
? recommendation.getHypothesisMatrix().getMatrix().size() : 0)
|
||||||
.targetingRecommendation(recommendation)
|
.targetingRecommendation(recommendation)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String buildTechnicalHealthSummary(TargetingRecommendationDto recommendation) {
|
||||||
|
if (recommendation == null || recommendation.getPixelHealth() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int enabled = 0;
|
||||||
|
if (recommendation.getPixelHealth().isFbp()) enabled++;
|
||||||
|
if (recommendation.getPixelHealth().isCapi()) enabled++;
|
||||||
|
if (recommendation.getPixelHealth().isGtm()) enabled++;
|
||||||
|
|
||||||
|
return switch (enabled) {
|
||||||
|
case 3 -> "Strong";
|
||||||
|
case 2 -> "Medium";
|
||||||
|
default -> "Poor";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private TargetingAudienceProfile buildAudienceProfile(
|
private TargetingAudienceProfile buildAudienceProfile(
|
||||||
TargetingRecommendationDto recommendation,
|
TargetingRecommendationDto recommendation,
|
||||||
kz.konturai.parser.dto.AudienceOverrideDto overrideDto) {
|
kz.konturai.parser.dto.AudienceOverrideDto overrideDto) {
|
||||||
|
|||||||
@@ -109,9 +109,6 @@ deep-research.api.timeout=1800000
|
|||||||
|
|
||||||
security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
security.jwt.secret-base64=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||||
|
|
||||||
# Claude (Anthropic) API — для AI предсказаний кампаний
|
|
||||||
claude.api.key=${CLAUDE_API_KEY:}
|
|
||||||
|
|
||||||
encryption.secret-key=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
encryption.secret-key=ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl
|
||||||
|
|
||||||
posting.scheduler.enabled=true
|
posting.scheduler.enabled=true
|
||||||
|
|||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
|
||||||
|
import kz.konturai.parser.dto.targeting.AudienceProfileDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.HypothesisCellDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.HypothesisMatrixDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.LandingPageAuditDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.RiskAnalysisDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingBudgetDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||||
|
import kz.konturai.parser.enums.AverageCheck;
|
||||||
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class AiTargetingSystemServiceManagementLayerTest {
|
||||||
|
|
||||||
|
private final AiTargetingSystemService service =
|
||||||
|
new AiTargetingSystemService(null, new ObjectMapper(), null, null, null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validateAndNormalizeRecommendation_shouldFillManagementBlocksAndClampValues() throws Exception {
|
||||||
|
MarketingAnalysisV3Request req = new MarketingAnalysisV3Request();
|
||||||
|
req.setBusinessNiche("Стоматология");
|
||||||
|
req.setPromotionCities(List.of("Алматы"));
|
||||||
|
req.setAverageCheck(AverageCheck.HIGH);
|
||||||
|
req.setUserSocialLinks(List.of("https://site.kz", "https://instagram.com/brand"));
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result.CompetitionIntensity intensity = new MarketingAnalysisV3Result.CompetitionIntensity();
|
||||||
|
intensity.setCiiIndex(82);
|
||||||
|
intensity.setIntensityLabel("Высокий");
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result.CompetitorProfile competitor = new MarketingAnalysisV3Result.CompetitorProfile();
|
||||||
|
competitor.setName("Strong Clinic");
|
||||||
|
competitor.setFollowers(12000);
|
||||||
|
competitor.setStrengths(List.of("Много видео-отзывов", "Сильный social proof"));
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result res = new MarketingAnalysisV3Result();
|
||||||
|
res.setCompetitionIntensity(intensity);
|
||||||
|
res.setCompetitorMap(List.of(competitor));
|
||||||
|
|
||||||
|
MarketingStrategy strategy = new MarketingStrategy();
|
||||||
|
strategy.setScoringModelName("TRUST");
|
||||||
|
strategy.setScoringModelTitle("Trust");
|
||||||
|
strategy.setStatus("completed");
|
||||||
|
strategy.setDurationWeeks(4);
|
||||||
|
|
||||||
|
TargetingRecommendationDto partial = TargetingRecommendationDto.builder()
|
||||||
|
.recommendedType("MIXED")
|
||||||
|
.recommendedTypeLabel("Комбинированный таргетинг")
|
||||||
|
.typeRationale("Базовая логика")
|
||||||
|
.campaignObjective("LEADS")
|
||||||
|
.campaignObjectiveLabel("Лиды")
|
||||||
|
.campaignObjectiveRationale("Нужно больше заявок")
|
||||||
|
.audienceProfile(AudienceProfileDto.builder()
|
||||||
|
.primarySegment("Жители Алматы")
|
||||||
|
.ageRange("25-44")
|
||||||
|
.gender("Женщины 60%, Мужчины 40%")
|
||||||
|
.build())
|
||||||
|
.budgetRecommendation(TargetingBudgetDto.builder()
|
||||||
|
.dailyBudget("10 000 KZT")
|
||||||
|
.monthlyBudget("300 000 KZT")
|
||||||
|
.build())
|
||||||
|
.hypothesisMatrix(HypothesisMatrixDto.builder()
|
||||||
|
.rowCount(1)
|
||||||
|
.colCount(1)
|
||||||
|
.rows(List.of("Только один angle"))
|
||||||
|
.cols(List.of("Только один visual"))
|
||||||
|
.matrix(List.of(HypothesisCellDto.builder()
|
||||||
|
.hookIndex(0)
|
||||||
|
.visualIndex(0)
|
||||||
|
.specificAdIdea("")
|
||||||
|
.build()))
|
||||||
|
.build())
|
||||||
|
.lpAudit(LandingPageAuditDto.builder()
|
||||||
|
.matchScore(140)
|
||||||
|
.criticalFixes(List.of())
|
||||||
|
.trustBoosters(List.of())
|
||||||
|
.build())
|
||||||
|
.risks(RiskAnalysisDto.builder()
|
||||||
|
.policyRiskLevel("высокий")
|
||||||
|
.moderationTriggers(List.of())
|
||||||
|
.build())
|
||||||
|
.suggestedRules(List.of())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Method method = AiTargetingSystemService.class.getDeclaredMethod(
|
||||||
|
"validateAndNormalizeRecommendation",
|
||||||
|
TargetingRecommendationDto.class,
|
||||||
|
MarketingAnalysisV3Request.class,
|
||||||
|
MarketingAnalysisV3Result.class,
|
||||||
|
MarketingStrategy.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
TargetingRecommendationDto normalized = (TargetingRecommendationDto) method.invoke(
|
||||||
|
service, partial, req, res, strategy);
|
||||||
|
|
||||||
|
assertNotNull(normalized.getPixelHealth());
|
||||||
|
assertNotNull(normalized.getFunnelStrategy());
|
||||||
|
assertNotNull(normalized.getHypothesisMatrix());
|
||||||
|
assertNotNull(normalized.getLpAudit());
|
||||||
|
assertNotNull(normalized.getScalingPlan());
|
||||||
|
assertNotNull(normalized.getRisks());
|
||||||
|
assertEquals(4, normalized.getHypothesisMatrix().getRowCount());
|
||||||
|
assertEquals(4, normalized.getHypothesisMatrix().getColCount());
|
||||||
|
assertEquals(16, normalized.getHypothesisMatrix().getMatrix().size());
|
||||||
|
assertEquals(100, normalized.getLpAudit().getMatchScore());
|
||||||
|
assertEquals(3, normalized.getLpAudit().getCriticalFixes().size());
|
||||||
|
assertEquals(3, normalized.getLpAudit().getTrustBoosters().size());
|
||||||
|
assertEquals(3, normalized.getSuggestedRules().size());
|
||||||
|
assertEquals(3, normalized.getRisks().getModerationTriggers().size());
|
||||||
|
assertTrue(normalized.getRisks().getPolicyRiskLevel().startsWith("High"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildEnhancedFullContext_shouldIncludeContextEnrichment() throws Exception {
|
||||||
|
MarketingAnalysisV3Request req = new MarketingAnalysisV3Request();
|
||||||
|
req.setBusinessNiche("Онлайн-курсы");
|
||||||
|
req.setProductName("Course X");
|
||||||
|
req.setGoal("Рост лидов");
|
||||||
|
req.setPromotionCities(List.of("Астана"));
|
||||||
|
req.setUserSocialLinks(List.of("https://site.kz", "https://instagram.com/coursex"));
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result.CompetitionIntensity intensity = new MarketingAnalysisV3Result.CompetitionIntensity();
|
||||||
|
intensity.setCiiIndex(68);
|
||||||
|
intensity.setIntensityLabel("Средний");
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result.ContentProfile contentProfile = new MarketingAnalysisV3Result.ContentProfile();
|
||||||
|
contentProfile.setVideoShare(74.0);
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result.CompetitorProfile competitor = new MarketingAnalysisV3Result.CompetitorProfile();
|
||||||
|
competitor.setName("Competitor A");
|
||||||
|
competitor.setFollowers(9000);
|
||||||
|
competitor.setStrengths(List.of("Сильный экспертный контент"));
|
||||||
|
|
||||||
|
MarketingAnalysisV3Result res = new MarketingAnalysisV3Result();
|
||||||
|
res.setCompetitionIntensity(intensity);
|
||||||
|
res.setContentProfile(contentProfile);
|
||||||
|
res.setCompetitorMap(List.of(competitor));
|
||||||
|
|
||||||
|
MarketingStrategy strategy = new MarketingStrategy();
|
||||||
|
strategy.setScoringModelName("AUTHORITY");
|
||||||
|
strategy.setScoringModelTitle("Authority");
|
||||||
|
strategy.setStatus("completed");
|
||||||
|
strategy.setDurationWeeks(4);
|
||||||
|
|
||||||
|
Method method = AiTargetingSystemService.class.getDeclaredMethod(
|
||||||
|
"buildEnhancedFullContext",
|
||||||
|
MarketingAnalysisV3Request.class,
|
||||||
|
MarketingAnalysisV3Result.class,
|
||||||
|
MarketingStrategy.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
String context = (String) method.invoke(service, req, res, strategy);
|
||||||
|
|
||||||
|
assertTrue(context.contains("https://instagram.com/coursex"));
|
||||||
|
assertTrue(context.contains("Competitor A"));
|
||||||
|
assertTrue(context.contains("AUTHORITY"));
|
||||||
|
assertTrue(context.contains("CURRENT DIGITAL ASSETS"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package kz.konturai.parser.service;
|
||||||
|
|
||||||
|
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
|
||||||
|
import kz.konturai.parser.dto.TargetingAIAnalysisResult;
|
||||||
|
import kz.konturai.parser.dto.targeting.HypothesisCellDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.HypothesisMatrixDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.LandingPageAuditDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.PixelCheckDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.RiskAnalysisDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingOrchestrationDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||||
|
import kz.konturai.parser.model.MarketingAnalysisV3Document;
|
||||||
|
import kz.konturai.parser.model.MarketingStrategy;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
|
||||||
|
class TargetingCampaignServiceManagementLayerTest {
|
||||||
|
|
||||||
|
private final TargetingCampaignService service =
|
||||||
|
new TargetingCampaignService(null, null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildAiAnalysisResult_shouldPopulateSummaryFields() throws Exception {
|
||||||
|
MarketingStrategy strategy = new MarketingStrategy();
|
||||||
|
strategy.setId("strategy-1");
|
||||||
|
strategy.setAnalysisId("analysis-1");
|
||||||
|
|
||||||
|
MarketingAnalysisV3Request request = new MarketingAnalysisV3Request();
|
||||||
|
request.setBusinessNiche("Автосервис");
|
||||||
|
request.setProductName("Oil Change");
|
||||||
|
request.setGoal("Лиды");
|
||||||
|
|
||||||
|
MarketingAnalysisV3Document analysis = new MarketingAnalysisV3Document();
|
||||||
|
analysis.setRequestData(request);
|
||||||
|
|
||||||
|
List<HypothesisCellDto> cells = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
cells.add(HypothesisCellDto.builder()
|
||||||
|
.hookIndex(i / 4)
|
||||||
|
.visualIndex(i % 4)
|
||||||
|
.specificAdIdea("Idea " + i)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TargetingOrchestrationDto.AutomationRuleDto> rules = List.of(
|
||||||
|
buildRule("rule-1"),
|
||||||
|
buildRule("rule-2"),
|
||||||
|
buildRule("rule-3"));
|
||||||
|
|
||||||
|
TargetingRecommendationDto recommendation = TargetingRecommendationDto.builder()
|
||||||
|
.recommendedType("MIXED")
|
||||||
|
.recommendedTypeLabel("Комбинированный")
|
||||||
|
.campaignObjective("LEADS")
|
||||||
|
.questionnaireSignals(List.of("signal"))
|
||||||
|
.typeRationale("reason")
|
||||||
|
.quickWins(List.of("win"))
|
||||||
|
.warnings(List.of("warning"))
|
||||||
|
.pixelHealth(PixelCheckDto.builder().fbp(true).capi(true).gtm(true).build())
|
||||||
|
.lpAudit(LandingPageAuditDto.builder().matchScore(88).build())
|
||||||
|
.risks(RiskAnalysisDto.builder().policyRiskLevel("High - высокий риск").build())
|
||||||
|
.suggestedRules(rules)
|
||||||
|
.hypothesisMatrix(HypothesisMatrixDto.builder().matrix(cells).build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Method method = TargetingCampaignService.class.getDeclaredMethod(
|
||||||
|
"buildAiAnalysisResult",
|
||||||
|
MarketingStrategy.class,
|
||||||
|
MarketingAnalysisV3Document.class,
|
||||||
|
TargetingRecommendationDto.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
TargetingAIAnalysisResult result = (TargetingAIAnalysisResult) method.invoke(
|
||||||
|
service, strategy, analysis, recommendation);
|
||||||
|
|
||||||
|
assertEquals("Strong", result.getTechnicalHealthSummary());
|
||||||
|
assertEquals(88, result.getLpMatchScore());
|
||||||
|
assertEquals("High - высокий риск", result.getPolicyRiskLevel());
|
||||||
|
assertEquals(3, result.getSuggestedAutomationRulesCount());
|
||||||
|
assertEquals(16, result.getMatrixCellsCount());
|
||||||
|
assertSame(recommendation, result.getTargetingRecommendation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingOrchestrationDto.AutomationRuleDto buildRule(String id) {
|
||||||
|
return TargetingOrchestrationDto.AutomationRuleDto.builder()
|
||||||
|
.ruleId(id)
|
||||||
|
.entityLevel("ADSET")
|
||||||
|
.conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
|
||||||
|
.conditions(List.of(TargetingOrchestrationDto.ConditionDto.builder()
|
||||||
|
.metric("spend")
|
||||||
|
.operator(TargetingOrchestrationDto.Operator.GREATER_THAN)
|
||||||
|
.value(BigDecimal.TEN)
|
||||||
|
.build()))
|
||||||
|
.action("PAUSE")
|
||||||
|
.actionValue(BigDecimal.ZERO)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user