target fix
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(curl -s http://localhost:8080/api/targeting/analysis -X POST -H \"Content-Type: application/json\" -d '{\"topic\":\"test\"}')",
|
||||||
|
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{\"topic\":\"\"}')",
|
||||||
|
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{\"topic\":\"ab\"}')",
|
||||||
|
"Bash(curl -s -X POST http://localhost:8080/api/targeting/analysis -H 'Content-Type: application/json' -d '{}')",
|
||||||
|
"Bash(curl -s -X POST http://localhost:8080/api/targeting/strategy -H 'Content-Type: application/json' -d '{\"analysis\": null}')",
|
||||||
|
"Bash(curl -s -X POST http://localhost:8080/api/targeting/creative -H 'Content-Type: application/json' -d '{\"captionIdea\": \"\"}')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package kz.konturai.parser.dto;
|
package kz.konturai.parser.dto;
|
||||||
|
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -12,8 +13,20 @@ import java.util.List;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class TargetingAIAnalysisResult {
|
public class TargetingAIAnalysisResult {
|
||||||
|
private String strategyId;
|
||||||
|
private String analysisId;
|
||||||
|
private String businessNiche;
|
||||||
|
private String productName;
|
||||||
|
private String goal;
|
||||||
|
private String recommendedTargetingType;
|
||||||
|
private String recommendedTargetingTypeLabel;
|
||||||
|
private String recommendedCampaignObjective;
|
||||||
|
private List<String> questionnaireSignals;
|
||||||
private List<AudienceSegmentDto> audienceSegments;
|
private List<AudienceSegmentDto> audienceSegments;
|
||||||
private List<AudienceSegmentDto> competitorInsights;
|
private List<AudienceSegmentDto> competitorInsights;
|
||||||
private BudgetOptimizationDto budgetOptimization;
|
private BudgetOptimizationDto budgetOptimization;
|
||||||
private String aiRecommendationsRationale;
|
private String aiRecommendationsRationale;
|
||||||
|
private List<String> quickWins;
|
||||||
|
private List<String> warnings;
|
||||||
|
private TargetingRecommendationDto targetingRecommendation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import java.util.List;
|
|||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class TargetingCampaignRequest {
|
public class TargetingCampaignRequest {
|
||||||
private String analysisId;
|
|
||||||
private String strategyId;
|
private String strategyId;
|
||||||
private String campaignName;
|
private String campaignName;
|
||||||
private String objective;
|
private String objective;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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 AudienceProfileDto {
|
||||||
|
|
||||||
|
/** Основной сегмент — кто это люди */
|
||||||
|
private String primarySegment;
|
||||||
|
|
||||||
|
/** Дополнительный сегмент */
|
||||||
|
private String secondarySegment;
|
||||||
|
|
||||||
|
/** Возрастной диапазон, например "28-45" */
|
||||||
|
private String ageRange;
|
||||||
|
|
||||||
|
/** Пол, например "Женщины 70%, Мужчины 30%" */
|
||||||
|
private String gender;
|
||||||
|
|
||||||
|
/** Интересы для таргетинга */
|
||||||
|
private List<String> interests;
|
||||||
|
|
||||||
|
/** Поведенческие паттерны для таргетинга */
|
||||||
|
private List<String> behaviors;
|
||||||
|
|
||||||
|
/** Города */
|
||||||
|
private List<String> geography;
|
||||||
|
|
||||||
|
/** Исключения из аудитории */
|
||||||
|
private List<String> exclusions;
|
||||||
|
|
||||||
|
/** Язык аудитории */
|
||||||
|
private String language;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package kz.konturai.parser.dto.targeting;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PublishedPostDto {
|
||||||
|
|
||||||
|
private int postIndex;
|
||||||
|
private String platform;
|
||||||
|
private String contentType;
|
||||||
|
private String theme;
|
||||||
|
private String postText;
|
||||||
|
private List<String> hashtags;
|
||||||
|
private String imageUrl;
|
||||||
|
private boolean imagePosted;
|
||||||
|
private String facebookPostId;
|
||||||
|
|
||||||
|
/** PUBLISHED | TEXT_ONLY | FAILED */
|
||||||
|
private String facebookStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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 TargetingBudgetDto {
|
||||||
|
|
||||||
|
/** Рекомендуемый дневной бюджет */
|
||||||
|
private String dailyBudget;
|
||||||
|
|
||||||
|
/** Рекомендуемый месячный бюджет */
|
||||||
|
private String monthlyBudget;
|
||||||
|
|
||||||
|
/** Стратегия ставок */
|
||||||
|
private String bidStrategy;
|
||||||
|
|
||||||
|
/** Обоснование бюджета */
|
||||||
|
private String rationale;
|
||||||
|
|
||||||
|
/** Ожидаемая стоимость клика */
|
||||||
|
private String estimatedCpc;
|
||||||
|
|
||||||
|
/** Ожидаемая стоимость лида */
|
||||||
|
private String estimatedCpl;
|
||||||
|
|
||||||
|
/** Ожидаемый охват в день */
|
||||||
|
private String estimatedDailyReach;
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -18,24 +16,15 @@ public class TargetingLaunchResultDto {
|
|||||||
private String analysisId;
|
private String analysisId;
|
||||||
private String scoringModelName;
|
private String scoringModelName;
|
||||||
private String scoringModelTitle;
|
private String scoringModelTitle;
|
||||||
|
private String businessNiche;
|
||||||
|
private String goal;
|
||||||
|
|
||||||
// ── Первый опубликованный пост ────────────────────────────────────────────
|
// ── AI-рекомендация таргетинга (главный объект) ───────────────────────────
|
||||||
private int postIndex;
|
private TargetingRecommendationDto targetingRecommendation;
|
||||||
private String platform;
|
|
||||||
private String contentType; // "фото" / "видео"
|
|
||||||
private String theme;
|
|
||||||
private String postText;
|
|
||||||
private List<String> hashtags;
|
|
||||||
private String imageUrl;
|
|
||||||
private boolean imagePosted;
|
|
||||||
|
|
||||||
// ── Facebook ──────────────────────────────────────────────────────────────
|
// ── Опубликованный первый пост ────────────────────────────────────────────
|
||||||
private String facebookPostId;
|
private PublishedPostDto publishedPost;
|
||||||
private String facebookStatus; // "PUBLISHED" | "FAILED" | "TEXT_ONLY"
|
|
||||||
|
|
||||||
// ── Таргетинг (генерируется AI из V3 анализа) ────────────────────────────
|
// ── Итоговое сообщение ────────────────────────────────────────────────────
|
||||||
private TargetingSettingsDto targetingSettings;
|
|
||||||
|
|
||||||
// ── Сводка ───────────────────────────────────────────────────────────────
|
|
||||||
private String message;
|
private String message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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 TargetingPhaseDto {
|
||||||
|
|
||||||
|
private int phase;
|
||||||
|
|
||||||
|
/** Название фазы */
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/** Продолжительность */
|
||||||
|
private String duration;
|
||||||
|
|
||||||
|
/** Тип таргетинга в этой фазе */
|
||||||
|
private String targetingType;
|
||||||
|
|
||||||
|
/** Цель фазы */
|
||||||
|
private String objective;
|
||||||
|
|
||||||
|
/** Аудитория для этой фазы */
|
||||||
|
private String audience;
|
||||||
|
|
||||||
|
/** Рекламные форматы */
|
||||||
|
private List<String> adFormats;
|
||||||
|
|
||||||
|
/** Бюджет на фазу */
|
||||||
|
private String budget;
|
||||||
|
|
||||||
|
/** KPI */
|
||||||
|
private String kpi;
|
||||||
|
|
||||||
|
/** Тактики */
|
||||||
|
private List<String> tactics;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
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 TargetingRecommendationDto {
|
||||||
|
|
||||||
|
// ── Тип таргетинга ────────────────────────────────────────────────────────
|
||||||
|
/**
|
||||||
|
* Рекомендуемый тип: COLD_INTEREST / BEHAVIORAL / LOOKALIKE /
|
||||||
|
* RETARGETING / LEAD_GEN / AWARENESS / MIXED
|
||||||
|
*/
|
||||||
|
private String recommendedType;
|
||||||
|
|
||||||
|
/** Читаемое название типа */
|
||||||
|
private String recommendedTypeLabel;
|
||||||
|
|
||||||
|
/** Почему AI выбрал именно этот тип — на основе данных опросника */
|
||||||
|
private String typeRationale;
|
||||||
|
|
||||||
|
// ── Цель кампании ─────────────────────────────────────────────────────────
|
||||||
|
/** AWARENESS / TRAFFIC / LEADS / ENGAGEMENT / SALES */
|
||||||
|
private String campaignObjective;
|
||||||
|
|
||||||
|
/** Читаемая цель */
|
||||||
|
private String campaignObjectiveLabel;
|
||||||
|
|
||||||
|
/** Обоснование цели */
|
||||||
|
private String campaignObjectiveRationale;
|
||||||
|
|
||||||
|
// ── Аудитория ─────────────────────────────────────────────────────────────
|
||||||
|
private AudienceProfileDto audienceProfile;
|
||||||
|
|
||||||
|
// ── Платформы и форматы ───────────────────────────────────────────────────
|
||||||
|
private List<String> recommendedPlatforms;
|
||||||
|
private List<String> recommendedAdFormats;
|
||||||
|
private List<String> recommendedPlacements;
|
||||||
|
|
||||||
|
// ── Бюджет ────────────────────────────────────────────────────────────────
|
||||||
|
private TargetingBudgetDto budgetRecommendation;
|
||||||
|
|
||||||
|
// ── Фазы таргетинга ───────────────────────────────────────────────────────
|
||||||
|
private List<TargetingPhaseDto> phases;
|
||||||
|
|
||||||
|
// ── KPI ───────────────────────────────────────────────────────────────────
|
||||||
|
private String estimatedCtr;
|
||||||
|
private String estimatedCpl;
|
||||||
|
private String estimatedReach;
|
||||||
|
private String estimatedFrequency;
|
||||||
|
private String estimatedConversions;
|
||||||
|
|
||||||
|
// ── Ключевые выводы из опросника ──────────────────────────────────────────
|
||||||
|
/** Сигналы из опросника, которые повлияли на рекомендацию */
|
||||||
|
private List<String> questionnaireSinals;
|
||||||
|
|
||||||
|
// ── Конкурентный контекст ─────────────────────────────────────────────────
|
||||||
|
private String competitiveContext;
|
||||||
|
private String ciiLevel;
|
||||||
|
private String differentiationAdvice;
|
||||||
|
|
||||||
|
// ── Быстрые победы и предупреждения ──────────────────────────────────────
|
||||||
|
private List<String> quickWins;
|
||||||
|
private List<String> warnings;
|
||||||
|
|
||||||
|
// ── Контент-стратегия ─────────────────────────────────────────────────────
|
||||||
|
private List<String> contentMix;
|
||||||
|
private String postingFrequency;
|
||||||
|
private List<String> bestPostingTimes;
|
||||||
|
|
||||||
|
// ── CTA и воронка ─────────────────────────────────────────────────────────
|
||||||
|
private String primaryCta;
|
||||||
|
private String funnelStage;
|
||||||
|
private String conversionMechanism;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ import kz.konturai.parser.dto.StatusHistoryEntry;
|
|||||||
import kz.konturai.parser.dto.TargetingAIAnalysisResult;
|
import kz.konturai.parser.dto.TargetingAIAnalysisResult;
|
||||||
import kz.konturai.parser.dto.TargetingCampaignRequest;
|
import kz.konturai.parser.dto.TargetingCampaignRequest;
|
||||||
import kz.konturai.parser.dto.TargetingPerformanceDto;
|
import kz.konturai.parser.dto.TargetingPerformanceDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.AudienceProfileDto;
|
||||||
|
import kz.konturai.parser.dto.targeting.TargetingRecommendationDto;
|
||||||
import kz.konturai.parser.exception.TargetingCampaignNotFoundException;
|
import kz.konturai.parser.exception.TargetingCampaignNotFoundException;
|
||||||
import kz.konturai.parser.model.*;
|
import kz.konturai.parser.model.*;
|
||||||
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
|
||||||
@@ -22,9 +24,13 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -34,6 +40,7 @@ public class TargetingCampaignService {
|
|||||||
private final TargetingCampaignRepository repository;
|
private final TargetingCampaignRepository repository;
|
||||||
private final MarketingAnalysisV3Repository analysisRepository;
|
private final MarketingAnalysisV3Repository analysisRepository;
|
||||||
private final AiTargetologistService aiService;
|
private final AiTargetologistService aiService;
|
||||||
|
private final AiTargetingSystemService aiTargetingSystemService;
|
||||||
private final ABTestingService abTestingService;
|
private final ABTestingService abTestingService;
|
||||||
private final FacebookAdsService facebookAdsService;
|
private final FacebookAdsService facebookAdsService;
|
||||||
private final TikTokAdsService tikTokAdsService;
|
private final TikTokAdsService tikTokAdsService;
|
||||||
@@ -42,21 +49,30 @@ public class TargetingCampaignService {
|
|||||||
private final MarketingStrategyRepository strategyRepository;
|
private final MarketingStrategyRepository strategyRepository;
|
||||||
|
|
||||||
public TargetingCampaign createCampaign(TargetingCampaignRequest req, String userId) {
|
public TargetingCampaign createCampaign(TargetingCampaignRequest req, String userId) {
|
||||||
if ((req.getAnalysisId() == null || req.getAnalysisId().isBlank()) && (req.getStrategyId() == null || req.getStrategyId().isBlank())) {
|
if (req.getStrategyId() == null || req.getStrategyId().isBlank()) {
|
||||||
throw new IllegalArgumentException("analysisId or strategyId is required");
|
throw new IllegalArgumentException("strategyId is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.getStrategyId() != null && !req.getStrategyId().isBlank()) {
|
MarketingStrategy strategy = strategyRepository.findById(req.getStrategyId())
|
||||||
MarketingStrategy strategy = strategyRepository.findById(req.getStrategyId())
|
.orElseThrow(() -> new IllegalArgumentException("Strategy not found: " + req.getStrategyId()));
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Strategy not found: " + req.getStrategyId()));
|
if (strategy.getAnalysisId() == null || strategy.getAnalysisId().isBlank()) {
|
||||||
if (req.getAnalysisId() == null || req.getAnalysisId().isBlank()) {
|
throw new IllegalArgumentException("Strategy does not contain analysisId: " + req.getStrategyId());
|
||||||
req.setAnalysisId(strategy.getAnalysisId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarketingAnalysisV3Document analysis = analysisRepository.findById(strategy.getAnalysisId())
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Analysis not found: " + strategy.getAnalysisId()));
|
||||||
|
|
||||||
if (req.getTotalBudgetKzt() == null || req.getTotalBudgetKzt() <= 0) {
|
if (req.getTotalBudgetKzt() == null || req.getTotalBudgetKzt() <= 0) {
|
||||||
throw new IllegalArgumentException("Valid budget is required");
|
throw new IllegalArgumentException("Valid budget is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TargetingRecommendationDto recommendation =
|
||||||
|
aiTargetingSystemService.generateLinkedRecommendation(strategy, analysis);
|
||||||
|
TargetingAudienceProfile audienceProfile =
|
||||||
|
buildAudienceProfile(recommendation, req.getAudienceOverride());
|
||||||
|
TargetingAIAnalysisResult aiAnalysisResult =
|
||||||
|
buildAiAnalysisResult(strategy, analysis, recommendation);
|
||||||
|
|
||||||
BudgetConfig budget = BudgetConfig.builder()
|
BudgetConfig budget = BudgetConfig.builder()
|
||||||
.totalBudget(req.getTotalBudgetKzt())
|
.totalBudget(req.getTotalBudgetKzt())
|
||||||
.dailyBudget(req.getDailyBudgetKzt())
|
.dailyBudget(req.getDailyBudgetKzt())
|
||||||
@@ -66,12 +82,14 @@ public class TargetingCampaignService {
|
|||||||
|
|
||||||
TargetingCampaign campaign = TargetingCampaign.builder()
|
TargetingCampaign campaign = TargetingCampaign.builder()
|
||||||
.userId(userId)
|
.userId(userId)
|
||||||
.analysisId(req.getAnalysisId())
|
.analysisId(strategy.getAnalysisId())
|
||||||
.strategyId(req.getStrategyId())
|
.strategyId(req.getStrategyId())
|
||||||
.name(req.getCampaignName())
|
.name(resolveCampaignName(req, strategy, recommendation))
|
||||||
.objective(CampaignObjective.valueOf(req.getObjective()))
|
.objective(resolveObjective(req, recommendation))
|
||||||
.status("draft")
|
.status("draft")
|
||||||
.budget(budget)
|
.budget(budget)
|
||||||
|
.audience(audienceProfile)
|
||||||
|
.aiRecommendations(aiAnalysisResult)
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.updatedAt(LocalDateTime.now())
|
.updatedAt(LocalDateTime.now())
|
||||||
.statusHistory(new ArrayList<>())
|
.statusHistory(new ArrayList<>())
|
||||||
@@ -80,13 +98,9 @@ public class TargetingCampaignService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Map platforms
|
// Map platforms
|
||||||
if (req.getPlatforms() != null) {
|
campaign.setPlatforms(resolvePlatforms(req, recommendation));
|
||||||
List<kz.konturai.parser.enums.TargetingPlatform> platforms = new ArrayList<>();
|
|
||||||
req.getPlatforms().forEach(p -> platforms.add(kz.konturai.parser.enums.TargetingPlatform.valueOf(p)));
|
|
||||||
campaign.setPlatforms(platforms);
|
|
||||||
}
|
|
||||||
|
|
||||||
addStatusHistory(campaign, "CREATED", "Campaign draft created, initiating AI pipeline");
|
addStatusHistory(campaign, "CREATED", "Campaign draft created from strategy with AI targeting recommendation");
|
||||||
TargetingCampaign saved = repository.save(campaign);
|
TargetingCampaign saved = repository.save(campaign);
|
||||||
|
|
||||||
// Process Async
|
// Process Async
|
||||||
@@ -108,12 +122,7 @@ public class TargetingCampaignService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarketingStrategy strategy = null;
|
MarketingStrategy strategy = strategyRepository.findById(campaign.getStrategyId()).orElse(null);
|
||||||
if (req.getStrategyId() != null) {
|
|
||||||
strategy = strategyRepository.findById(req.getStrategyId()).orElse(null);
|
|
||||||
} else {
|
|
||||||
strategy = strategyRepository.findFirstByAnalysisIdOrderByCreatedAtDesc(campaign.getAnalysisId()).orElse(null);
|
|
||||||
}
|
|
||||||
if (strategy == null) {
|
if (strategy == null) {
|
||||||
failCampaign(campaign, "Marketing Strategy containing media assets not found.");
|
failCampaign(campaign, "Marketing Strategy containing media assets not found.");
|
||||||
return;
|
return;
|
||||||
@@ -346,4 +355,202 @@ public class TargetingCampaignService {
|
|||||||
.message(message)
|
.message(message)
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CampaignObjective resolveObjective(
|
||||||
|
TargetingCampaignRequest req,
|
||||||
|
TargetingRecommendationDto recommendation) {
|
||||||
|
String objective = req.getObjective();
|
||||||
|
if (objective == null || objective.isBlank()) {
|
||||||
|
objective = recommendation != null ? recommendation.getCampaignObjective() : null;
|
||||||
|
}
|
||||||
|
if (objective == null || objective.isBlank()) {
|
||||||
|
return CampaignObjective.LEADS;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return CampaignObjective.valueOf(objective.trim().toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.warn("Unknown campaign objective '{}', fallback to LEADS", objective);
|
||||||
|
return CampaignObjective.LEADS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCampaignName(
|
||||||
|
TargetingCampaignRequest req,
|
||||||
|
MarketingStrategy strategy,
|
||||||
|
TargetingRecommendationDto recommendation) {
|
||||||
|
if (req.getCampaignName() != null && !req.getCampaignName().isBlank()) {
|
||||||
|
return req.getCampaignName().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> nameParts = new ArrayList<>();
|
||||||
|
nameParts.add("Targeting");
|
||||||
|
if (strategy.getScoringModelTitle() != null && !strategy.getScoringModelTitle().isBlank()) {
|
||||||
|
nameParts.add(strategy.getScoringModelTitle().trim());
|
||||||
|
}
|
||||||
|
if (recommendation != null
|
||||||
|
&& recommendation.getRecommendedTypeLabel() != null
|
||||||
|
&& !recommendation.getRecommendedTypeLabel().isBlank()) {
|
||||||
|
nameParts.add(recommendation.getRecommendedTypeLabel().trim());
|
||||||
|
}
|
||||||
|
return String.join(" | ", nameParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<kz.konturai.parser.enums.TargetingPlatform> resolvePlatforms(
|
||||||
|
TargetingCampaignRequest req,
|
||||||
|
TargetingRecommendationDto recommendation) {
|
||||||
|
List<String> rawPlatforms = req.getPlatforms();
|
||||||
|
if (rawPlatforms == null || rawPlatforms.isEmpty()) {
|
||||||
|
rawPlatforms = recommendation != null ? recommendation.getRecommendedPlatforms() : Collections.emptyList();
|
||||||
|
}
|
||||||
|
if (rawPlatforms == null || rawPlatforms.isEmpty()) {
|
||||||
|
return List.of(
|
||||||
|
kz.konturai.parser.enums.TargetingPlatform.INSTAGRAM,
|
||||||
|
kz.konturai.parser.enums.TargetingPlatform.FACEBOOK
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawPlatforms.stream()
|
||||||
|
.map(this::mapPlatform)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private kz.konturai.parser.enums.TargetingPlatform mapPlatform(String platform) {
|
||||||
|
if (platform == null || platform.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String normalized = platform.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if ("META".equals(normalized)) {
|
||||||
|
return kz.konturai.parser.enums.TargetingPlatform.FACEBOOK;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return kz.konturai.parser.enums.TargetingPlatform.valueOf(normalized);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
log.warn("Unknown targeting platform '{}', skipping", platform);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingAIAnalysisResult buildAiAnalysisResult(
|
||||||
|
MarketingStrategy strategy,
|
||||||
|
MarketingAnalysisV3Document analysis,
|
||||||
|
TargetingRecommendationDto recommendation) {
|
||||||
|
return TargetingAIAnalysisResult.builder()
|
||||||
|
.strategyId(strategy.getId())
|
||||||
|
.analysisId(strategy.getAnalysisId())
|
||||||
|
.businessNiche(analysis.getRequestData() != null ? analysis.getRequestData().getBusinessNiche() : null)
|
||||||
|
.productName(analysis.getRequestData() != null ? analysis.getRequestData().getProductName() : null)
|
||||||
|
.goal(analysis.getRequestData() != null ? analysis.getRequestData().getGoal() : null)
|
||||||
|
.recommendedTargetingType(recommendation != null ? recommendation.getRecommendedType() : null)
|
||||||
|
.recommendedTargetingTypeLabel(recommendation != null ? recommendation.getRecommendedTypeLabel() : null)
|
||||||
|
.recommendedCampaignObjective(recommendation != null ? recommendation.getCampaignObjective() : null)
|
||||||
|
.questionnaireSignals(recommendation != null ? recommendation.getQuestionnaireSinals() : List.of())
|
||||||
|
.aiRecommendationsRationale(recommendation != null ? recommendation.getTypeRationale() : null)
|
||||||
|
.quickWins(recommendation != null ? recommendation.getQuickWins() : List.of())
|
||||||
|
.warnings(recommendation != null ? recommendation.getWarnings() : List.of())
|
||||||
|
.targetingRecommendation(recommendation)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TargetingAudienceProfile buildAudienceProfile(
|
||||||
|
TargetingRecommendationDto recommendation,
|
||||||
|
kz.konturai.parser.dto.AudienceOverrideDto overrideDto) {
|
||||||
|
AudienceProfileDto aiAudience = recommendation != null ? recommendation.getAudienceProfile() : null;
|
||||||
|
|
||||||
|
int[] ageRange = parseAgeRange(aiAudience != null ? aiAudience.getAgeRange() : null);
|
||||||
|
int ageMin = overrideDto != null && overrideDto.getAgeMin() > 0 ? overrideDto.getAgeMin() : ageRange[0];
|
||||||
|
int ageMax = overrideDto != null && overrideDto.getAgeMax() > 0 ? overrideDto.getAgeMax() : ageRange[1];
|
||||||
|
|
||||||
|
List<String> interests = new ArrayList<>();
|
||||||
|
if (aiAudience != null && aiAudience.getInterests() != null) {
|
||||||
|
interests.addAll(aiAudience.getInterests());
|
||||||
|
}
|
||||||
|
if (overrideDto != null && overrideDto.getAdditionalInterests() != null) {
|
||||||
|
interests.addAll(overrideDto.getAdditionalInterests());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> cities = new ArrayList<>();
|
||||||
|
if (overrideDto != null && overrideDto.getCities() != null && !overrideDto.getCities().isEmpty()) {
|
||||||
|
cities.addAll(overrideDto.getCities());
|
||||||
|
} else if (aiAudience != null && aiAudience.getGeography() != null) {
|
||||||
|
cities.addAll(aiAudience.getGeography());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> genders = overrideDto != null && overrideDto.getGenders() != null && !overrideDto.getGenders().isEmpty()
|
||||||
|
? overrideDto.getGenders()
|
||||||
|
: inferGenders(aiAudience != null ? aiAudience.getGender() : null);
|
||||||
|
|
||||||
|
List<LocationTarget> locations = cities.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(city -> !city.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.map(city -> LocationTarget.builder()
|
||||||
|
.cityName(city)
|
||||||
|
.country("KZ")
|
||||||
|
.radius(25.0)
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return TargetingAudienceProfile.builder()
|
||||||
|
.ageMin(ageMin)
|
||||||
|
.ageMax(ageMax)
|
||||||
|
.genders(genders)
|
||||||
|
.locations(locations)
|
||||||
|
.interests(interests.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isBlank()).distinct().collect(Collectors.toList()))
|
||||||
|
.behaviors(aiAudience != null && aiAudience.getBehaviors() != null ? aiAudience.getBehaviors() : List.of())
|
||||||
|
.languages(splitLanguages(aiAudience != null ? aiAudience.getLanguage() : null))
|
||||||
|
.excludedAudiences(aiAudience != null && aiAudience.getExclusions() != null ? aiAudience.getExclusions() : List.of())
|
||||||
|
.deviceTypes(List.of("mobile", "desktop"))
|
||||||
|
.connectionType("ALL")
|
||||||
|
.audienceScore(85)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] parseAgeRange(String rawAgeRange) {
|
||||||
|
if (rawAgeRange == null || rawAgeRange.isBlank()) {
|
||||||
|
return new int[]{25, 45};
|
||||||
|
}
|
||||||
|
String[] parts = rawAgeRange.replaceAll("[^0-9-]", "").split("-");
|
||||||
|
if (parts.length == 2) {
|
||||||
|
try {
|
||||||
|
return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])};
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return new int[]{25, 45};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new int[]{25, 45};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> inferGenders(String genderText) {
|
||||||
|
if (genderText == null || genderText.isBlank()) {
|
||||||
|
return List.of("ALL");
|
||||||
|
}
|
||||||
|
String normalized = genderText.toLowerCase(Locale.ROOT);
|
||||||
|
boolean hasWomen = normalized.contains("жен");
|
||||||
|
boolean hasMen = normalized.contains("муж");
|
||||||
|
if (hasWomen && hasMen) {
|
||||||
|
return List.of("FEMALE", "MALE");
|
||||||
|
}
|
||||||
|
if (hasWomen) {
|
||||||
|
return List.of("FEMALE");
|
||||||
|
}
|
||||||
|
if (hasMen) {
|
||||||
|
return List.of("MALE");
|
||||||
|
}
|
||||||
|
return List.of("ALL");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitLanguages(String languagesText) {
|
||||||
|
if (languagesText == null || languagesText.isBlank()) {
|
||||||
|
return List.of("Русский");
|
||||||
|
}
|
||||||
|
return List.of(languagesText.split(","))
|
||||||
|
.stream()
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user