From e2786d53b0849f06a64c1e40fb8052af4703fa19 Mon Sep 17 00:00:00 2001 From: arys Date: Sun, 29 Mar 2026 20:51:34 +0500 Subject: [PATCH] target fix --- .../targeting/TargetingOrchestrationDto.java | 284 ++++++++++++++++++ .../service/AiTargetologistService.java | 251 +++++++++++----- .../service/TargetingCampaignService.java | 91 ++++-- 3 files changed, 517 insertions(+), 109 deletions(-) create mode 100644 src/main/java/kz/konturai/parser/dto/targeting/TargetingOrchestrationDto.java diff --git a/src/main/java/kz/konturai/parser/dto/targeting/TargetingOrchestrationDto.java b/src/main/java/kz/konturai/parser/dto/targeting/TargetingOrchestrationDto.java new file mode 100644 index 0000000..e762a6a --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/targeting/TargetingOrchestrationDto.java @@ -0,0 +1,284 @@ +package kz.konturai.parser.dto.targeting; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Root DTO for parsing the Targeting Orchestrator response. + * Maps the AI output to concrete typed objects. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class TargetingOrchestrationDto { + + @NotBlank + private String orchestrationId; + + @NotNull + private PlatformTarget platformTarget; + + @Valid + @NotNull + private List campaigns; + + @Valid + @NotNull + private List automationRules; + + public enum PlatformTarget { + META_ADS, GOOGLE_ADS, TIKTOK_ADS + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CampaignDto { + @NotBlank + private String campaignName; + + @NotBlank + private String objective; + + private List specialAdCategories; + + @NotBlank + private String buyingType; + + @NotNull + @Valid + private BudgetStrategyDto budgetStrategy; + + @NotNull + @Valid + private List adsets; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class BudgetStrategyDto { + @NotNull + private Boolean isCbo; + + private BigDecimal campaignDailyBudgetUsd; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AdSetDto { + @NotBlank + private String adsetName; + + @NotNull + @Min(0) + private BigDecimal dailyBudgetUsd; + + @NotBlank + private String optimizationGoal; + + @NotBlank + private String billingEvent; + + @NotBlank + private String bidStrategy; + + @NotNull + @Valid + private TargetingDto targeting; + + @NotNull + @Valid + private List ads; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class TargetingDto { + @NotNull + @Valid + private GeoLocationsDto geoLocations; + + @NotNull + private Integer ageMin; + + @NotNull + private Integer ageMax; + + private List genders; + + @Valid + private List flexibleSpec; + + @Valid + private ExclusionsDto exclusions; + + private List publisherPlatforms; + + private List devicePlatforms; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class GeoLocationsDto { + private List countries; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class FlexibleSpecDto { + @Valid + private List interests; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class InterestDto { + @NotBlank + private String id; + + @NotBlank + private String name; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class ExclusionsDto { + @Valid + private List customAudiences; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CustomAudienceDto { + @NotBlank + private String id; + + @NotBlank + private String name; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AdDto { + @NotBlank + private String adName; + + @NotNull + @Valid + private CreativePayloadDto creativePayload; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CreativePayloadDto { + @NotBlank + private String assetId; + + @NotBlank + private String bodyTextId; + + @NotBlank + private String headlineId; + + @NotBlank + private String callToAction; + + @NotBlank + private String destinationUrl; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AutomationRuleDto { + @NotBlank + private String ruleId; + + @NotBlank + private String entityLevel; + + @NotNull + private ConditionGroup conditionGroup; + + @NotNull + @Valid + private List conditions; + + @NotBlank + private String action; + + private BigDecimal actionValue; + } + + public enum ConditionGroup { + AND, OR + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class ConditionDto { + @NotBlank + private String metric; + + @NotNull + private Operator operator; + + @NotNull + private BigDecimal value; + } + + public enum Operator { + EQUAL, NOT_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN + } +} diff --git a/src/main/java/kz/konturai/parser/service/AiTargetologistService.java b/src/main/java/kz/konturai/parser/service/AiTargetologistService.java index 5c3cd2f..c06cab3 100644 --- a/src/main/java/kz/konturai/parser/service/AiTargetologistService.java +++ b/src/main/java/kz/konturai/parser/service/AiTargetologistService.java @@ -1,103 +1,202 @@ package kz.konturai.parser.service; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import kz.konturai.parser.dto.AudienceSegmentDto; -import kz.konturai.parser.dto.BudgetOptimizationDto; -import kz.konturai.parser.dto.TargetingCampaignRequest; +import kz.konturai.parser.dto.targeting.TargetingOrchestrationDto; import kz.konturai.parser.model.MarketingAnalysisV3Document; -import kz.konturai.parser.model.TargetingAd; -import kz.konturai.parser.model.TargetingAdSet; -import kz.konturai.parser.model.TargetingAudienceProfile; +import kz.konturai.parser.model.MarketingStrategy; +import kz.konturai.parser.model.SocialMediaCredentials; import kz.konturai.parser.model.TargetingCampaign; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.UUID; +import java.util.Map; @Service @RequiredArgsConstructor @Slf4j public class AiTargetologistService { - private final AudienceSegmentationService audienceService; - private final BudgetOptimizerService budgetOptimizerService; - private final ABTestingService abTestingService; private final OpenAIAnalyticsService openAiService; private final ObjectMapper objectMapper; - private final SerperSearchService serperSearchService; - public TargetingAudienceProfile generateAudienceProfile(String analysisId, TargetingCampaignRequest req, MarketingAnalysisV3Document analysis) { - log.info("Generating Audience Profile for analysis {}", analysisId); - - List aiSegments = audienceService.segmentAudience(analysis); - - TargetingAudienceProfile overrides = req.getAudienceOverride() != null ? TargetingAudienceProfile.builder() - .ageMin(req.getAudienceOverride().getAgeMin()) - .ageMax(req.getAudienceOverride().getAgeMax()) - .genders(req.getAudienceOverride().getGenders()) - .build() : new TargetingAudienceProfile(); - - return audienceService.mergeSegmentsWithExistingAudience(aiSegments, overrides); - } - - public List generateAdCreatives(TargetingCampaign campaign, MarketingAnalysisV3Document analysis) { - log.info("Generating Ad Creatives for campaign {}", campaign.getId()); - List adSets = new ArrayList<>(); + /** + * Executes the "Antigravity" Orchestration Pipeline. + * Maps business metrics, strategy data, and ready-made media assets into a strictly validated JSON payload. + * + * @param campaign The current campaign context (budget, goals, platforms). + * @param analysis The underlying strategy analysis (audiences, triggers, market). + * @param strategy The content strategy housing ready-made texts and media. + * @param credentials Ad Account credentials (to pass platform/ad-account IDs context). + * @return Fully populated JSON Orchestration DTO. + */ + public TargetingOrchestrationDto orchestrateCampaign( + TargetingCampaign campaign, + MarketingAnalysisV3Document analysis, + MarketingStrategy strategy, + List credentials) { + + log.info("[Antigravity Pipeline] Starting orchestration for campaign {}, analysisId: {}", campaign.getId(), analysis.getId()); try { - String context = objectMapper.writeValueAsString(analysis.getResultData()); - String objective = campaign.getObjective() != null ? campaign.getObjective().name() : "TRAFFIC"; + String systemPrompt = buildSystemMandate(); + String userPrompt = buildOrchestratorContext(campaign, analysis, strategy, credentials); - campaign.getPlatforms().forEach(platform -> { - TargetingAdSet adSet = TargetingAdSet.builder() - .adSetId(UUID.randomUUID().toString()) - .name(platform.name() + " Main Catch") - .platform(platform.name()) - .budget(campaign.getBudget().getDailyBudget() / campaign.getPlatforms().size()) - .bidStrategy("LOWEST_COST") - .optimizationGoal(objective) - .status("DRAFT") - .build(); + // Generate through strict OpenAI call. + // Notice: requesting max length for complex JSON tree, timeout scaled to 180s. + log.info("[Antigravity Pipeline] Sending context to Neural API..."); + String rawJsonResponse = openAiService.generateWithInstructionWithModel( + "", // text/context is baked into userPrompt + userPrompt, + "ru", + "gpt-4o", // High capability model required for strict JSON compliance and deep logic + systemPrompt, + 15000, + 180000L + ); - List ads = new ArrayList<>(); - if (campaign.getAbTestConfig() != null && campaign.getAbTestConfig().isEnabled()) { - ads.addAll(abTestingService.generateABVariants(context, objective)); - } else { - List defaultAds = abTestingService.generateABVariants(context, objective); - if (!defaultAds.isEmpty()) ads.add(defaultAds.get(0)); + if (rawJsonResponse == null || rawJsonResponse.isBlank()) { + throw new IllegalStateException("OpenAI returned empty orchestration payload"); + } + + // Cleanup potential markdown blocks if the model hallucinates them despite system prompt + String cleanJson = rawJsonResponse.replaceAll("(?s)^```json\\s*", "").replaceAll("(?s)\\s*```$", "").trim(); + log.debug("[Antigravity] Raw Orchestration JSON: {}", cleanJson); + + // Robust specific mapper + ObjectMapper strictMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + TargetingOrchestrationDto orchestration = strictMapper.readValue(cleanJson, TargetingOrchestrationDto.class); + log.info("[Antigravity Pipeline] Successfully parsed TargetingOrchestrationDto. Orchestration ID: {}", orchestration.getOrchestrationId()); + + return orchestration; + + } catch (Exception e) { + log.error("[Antigravity Pipeline] CRITICAL FAILURE during orchestration: {}", e.getMessage(), e); + throw new RuntimeException("AI Targeting Orchestration failed: " + e.getMessage(), e); + } + } + + private String buildSystemMandate() { + return """ + SYSTEM ROLE & ARCHITECTURE MANDATE + You are "Antigravity", an enterprise-grade AI Targeting Orchestrator and Media Buying Architect. Your sole function is to process sanitized marketing strategies and ready-made media assets, compiling them into a highly structured, production-ready payload for Ads Platform APIs (Meta Graph API, Google Ads API, TikTok Marketing API). + + You DO NOT generate basic raw creatives from scratch if ready-made texts exist. You DO NOT alter the core strategy. You map inputs to rigid targeting parameters, establish A/B testing matrices, calculate budget pacing, and define algorithmic optimization rules. + + OUTPUT CONTRACT: + Your output must be EXCLUSIVELY a valid JSON object. DO NOT wrap it in markdown block quotes. DO NOT include any natural language explanation, introductory text, or concluding remarks. Just raw valid JSON. + The JSON keys must be strictly snake_case conforming to the supplied schema structure. + """; + } + + private String buildOrchestratorContext( + TargetingCampaign campaign, + MarketingAnalysisV3Document analysis, + MarketingStrategy strategy, + List credentials) throws JsonProcessingException { + + // 1. Business Metrics Context + Map businessMetrics = new HashMap<>(); + businessMetrics.put("target_objective", campaign.getObjective() != null ? campaign.getObjective().name() : "TRAFFIC"); + businessMetrics.put("campaign_daily_budget_kzt", campaign.getBudget() != null ? campaign.getBudget().getDailyBudget() : 0); + businessMetrics.put("campaign_total_budget_kzt", campaign.getBudget() != null ? campaign.getBudget().getTotalBudget() : 0); + businessMetrics.put("target_platforms", campaign.getPlatforms()); + + // 2. Extracted Media Assets (Texts with hashtags) + List mediaAssets = strategy.getPostCalendar() != null ? strategy.getPostCalendar() : List.of(); + + StringBuilder prompt = new StringBuilder(); + prompt.append("INPUT CONTEXT (READ-ONLY):\n"); + prompt.append("1. \"business_metrics\": ").append(objectMapper.writeValueAsString(businessMetrics)).append("\n\n"); + prompt.append("2. \"strategy_matrix\": ").append(objectMapper.writeValueAsString(analysis.getResultData())).append("\n\n"); + prompt.append("3. \"media_assets\": ").append(objectMapper.writeValueAsString(mediaAssets)).append("\n\n"); + + prompt.append(""" + EXECUTION PROTOCOL + + PHASE 1: CAMPAIGN TOPOLOGY & NAMING CONVENTION + 1. Enforce strict naming conventions for parsing: [Platform]_[Objective]_[Geo]_[Date]_[UniqueIdentifier]. + 2. Determine Campaign Budget Optimization (CBO) vs. AdSet Budget Optimization (ABO) based on the number of audiences. Use ABO for strict A/B testing of specific segments; use CBO for scaling proven assets. + 3. Isolate variables. Never test more than one major variable (e.g., audience, creative format) per AdSet. + + PHASE 2: TARGETING ENGINE TRANSLATION + Map the "strategy_matrix" to precise API-level targeting parameters. + 1. Demographic Constraints: Calculate reasonable Min_Age, Max_Age based on the strategy. + 2. Detailed Targeting Matrix: Group interests logically. Select 2-4 HIGHLY relevant interests. + 3. Placements: Optimize dynamically based on objective. + + PHASE 3: THE A/B TESTING & CREATIVE MAPPING + 1. Map "media_assets" (texts, images, video paths, hashtags) exactly into your ads. USE SPECIFICALLY THE post_text AND hashtags PROVIDED. DO NOT hallucinate new ad copy if it is provided there. Match the content theme to the targeting AdSet. + 2. Ensure every Ad has proper UTM tracking parameters appended to destination_url: utm_source={{site_source_name}}&utm_medium={{placement}}&utm_campaign={{campaign.name}}&utm_content={{ad.name}}. + + PHASE 4: BUDGET ALLOCATION & PACING ALGORITHM + 1. Allocate daily budget into the created AdSets intelligently based on total campaign budget. + 2. Set optimization goals correctly (e.g., OFFSITE_CONVERSIONS or LINK_CLICKS). + + PHASE 5: RULES ENGINE (MICRO-OPTIMIZATION PROTOCOL) + Generate a strict set of logical conditions for the external cron-job/rules engine: + 1. EARLY_KILL: IF Spend > BudgetLimit AND actions < Threshold THEN Action = PAUSE. + 2. SCALE_UP: IF CPA < Target AND Frequency < 2.5 THEN Action = INCREASE_BUDGET. + + REQUIRED JSON SCHEMA STRUCTURE: + { + "orchestration_id": "uuid-v4", + "platform_target": "META_ADS", + "campaigns": [ + { + "campaign_name": "string", + "objective": "string", + "special_ad_categories": [], + "buying_type": "AUCTION", + "budget_strategy": { "is_cbo": false, "campaign_daily_budget_usd": 150.0 }, + "adsets": [ + { + "adset_name": "string", + "daily_budget_usd": 50.0, + "optimization_goal": "string", + "billing_event": "IMPRESSIONS", + "bid_strategy": "LOWEST_COST_WITHOUT_CAP", + "targeting": { + "geo_locations": { "countries": ["KZ"] }, + "age_min": 18, "age_max": 65, "genders": [1], + "flexible_spec": [ { "interests": [{ "id": "123", "name": "InterestName" }] } ], + "exclusions": {}, + "publisher_platforms": ["facebook", "instagram"], + "device_platforms": ["mobile"] + }, + "ads": [ + { + "ad_name": "string", + "creative_payload": { + "asset_id": "Use image_filename or video_filename from media_assets here", + "body_text_id": "Use EXACT post_text from media_assets here", + "headline_id": "Use EXACT theme from media_assets here", + "call_to_action": "LEARN_MORE", + "destination_url": "https://example.com/url_with_utms" + } + } + ] + } + ] } - - adSet.setAds(ads); - adSets.add(adSet); - }); - - } catch (Exception e) { - log.error("Error generating Ad creatives", e); - } + ], + "automation_rules": [ + { + "rule_id": "string", + "entity_level": "ADSET", + "condition_group": "AND", + "conditions": [ {"metric": "spend", "operator": "GREATER_THAN", "value": 75.0} ], + "action": "PAUSE", "action_value": 0.0 + } + ] + } + """); - return adSets; - } - - public BudgetOptimizationDto optimizeBudgetDistribution(TargetingCampaign campaign, MarketingAnalysisV3Document analysis) { - return budgetOptimizerService.optimizeBudget(campaign, analysis); - } - - public List analyzeCompetitorAds(String businessNiche, List cities) { - log.info("Analyzing Competitor Ads for niche {}", businessNiche); - try { - String query = businessNiche + " реклама кейсы " + String.join(" ", cities); - String searchResults = serperSearchService.search(query).toString(); - - // In a real scenario, feed searchResults into openAiService to parse out actionable segments - // For now, returning a static mapped list or empty list. - return new ArrayList<>(); - } catch (Exception e) { - log.error("Failed to analyze competitor ads", e); - return new ArrayList<>(); - } + return prompt.toString(); } } diff --git a/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java b/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java index 2e5a1c8..39c69c3 100644 --- a/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java +++ b/src/main/java/kz/konturai/parser/service/TargetingCampaignService.java @@ -7,7 +7,9 @@ import kz.konturai.parser.dto.TargetingPerformanceDto; import kz.konturai.parser.exception.TargetingCampaignNotFoundException; import kz.konturai.parser.model.*; import kz.konturai.parser.repository.MarketingAnalysisV3Repository; +import kz.konturai.parser.repository.MarketingStrategyRepository; import kz.konturai.parser.repository.TargetingCampaignRepository; +import kz.konturai.parser.dto.targeting.TargetingOrchestrationDto; import kz.konturai.parser.enums.CampaignObjective; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +39,7 @@ public class TargetingCampaignService { private final TikTokAdsService tikTokAdsService; private final SocialMediaCredentialsService credentialsService; private final TargetingInsightService insightService; + private final MarketingStrategyRepository strategyRepository; public TargetingCampaign createCampaign(TargetingCampaignRequest req, String userId) { if (req.getAnalysisId() == null || req.getAnalysisId().isBlank()) { @@ -86,7 +89,7 @@ public class TargetingCampaignService { @Async("targetingExecutor") public void processAsync(String campaignId, TargetingCampaignRequest req, String userId) { - log.info("Starting async processing for campaign {}", campaignId); + log.info("[Targeting Pipeline] Starting async Antigravity processing for campaign {}", campaignId); TargetingCampaign campaign = repository.findById(campaignId).orElseThrow(() -> new TargetingCampaignNotFoundException("Campaign not found async: " + campaignId)); @@ -97,54 +100,76 @@ public class TargetingCampaignService { return; } - // Step 1: AI Recommendations & Audience - addStatusHistory(campaign, "GENERATING_AUDIENCE", "Analyzing market data & segmenting audience"); - TargetingAudienceProfile profile = aiService.generateAudienceProfile(campaign.getAnalysisId(), req, analysis); - campaign.setAudience(profile); + MarketingStrategy strategy = null; + if (req.getStrategyId() != null) { + strategy = strategyRepository.findById(req.getStrategyId()).orElse(null); + } else { + strategy = strategyRepository.findByAnalysisId(campaign.getAnalysisId()).orElse(null); + } + if (strategy == null) { + failCampaign(campaign, "Marketing Strategy containing media assets not found."); + return; + } + + List credentials = credentialsService.getUserCredentials(userId); + + addStatusHistory(campaign, "ORCHESTRATING", "Running Antigravity AI Orchestrator to map strategy and text creatives to ad API payload"); - TargetingAIAnalysisResult aiResult = TargetingAIAnalysisResult.builder().build(); - campaign.setAiRecommendations(aiResult); + // 1. Run The Master AI Orchestrator Pipeline + TargetingOrchestrationDto payload = aiService.orchestrateCampaign(campaign, analysis, strategy, credentials); - // Step 2: Budget Optimization - addStatusHistory(campaign, "OPTIMIZING_BUDGET", "AI allocating budget efficiently"); - aiResult.setBudgetOptimization(aiService.optimizeBudgetDistribution(campaign, analysis)); + // 2. Map Payload to Local Database Models + addStatusHistory(campaign, "MAPPING", "Saving generated ad sets and creatives locally"); + List savedAdSets = new ArrayList<>(); + for (TargetingOrchestrationDto.CampaignDto apiCamp : payload.getCampaigns()) { + for (TargetingOrchestrationDto.AdSetDto apiAdSet : apiCamp.getAdsets()) { + TargetingAdSet adSet = TargetingAdSet.builder() + .adSetId(java.util.UUID.randomUUID().toString()) + .name(apiAdSet.getAdsetName()) + .budget(apiAdSet.getDailyBudgetUsd() != null ? apiAdSet.getDailyBudgetUsd().doubleValue() : 0.0) + .bidStrategy(apiAdSet.getBidStrategy()) + .optimizationGoal(apiAdSet.getOptimizationGoal()) + .status("DRAFT") + .build(); + + List ads = new ArrayList<>(); + if (apiAdSet.getAds() != null) { + for (TargetingOrchestrationDto.AdDto apiAd : apiAdSet.getAds()) { + TargetingAd ad = TargetingAd.builder() + .adId(java.util.UUID.randomUUID().toString()) + .name(apiAd.getAdName()) + .headline(apiAd.getCreativePayload().getHeadlineId()) + .primaryText(apiAd.getCreativePayload().getBodyTextId()) + .callToAction(apiAd.getCreativePayload().getCallToAction()) + .mediaUrl(apiAd.getCreativePayload().getAssetId()) + .build(); + ads.add(ad); + } + } + adSet.setAds(ads); + savedAdSets.add(adSet); + } + } + campaign.setAdSets(savedAdSets); repository.save(campaign); - // Step 3: Competitor Ads - addStatusHistory(campaign, "ANALYZING_COMPETITORS", "Reviewing competitor creatives"); - List cities = profile.getLocations() != null ? - profile.getLocations().stream().map(LocationTarget::getCityName).toList() : List.of("Алматы"); - aiResult.setCompetitorInsights(aiService.analyzeCompetitorAds("Ниша клиента", cities)); - - // Step 4: Ad Creatives - if (req.isGenerateAdCreatives()) { - addStatusHistory(campaign, "GENERATING_CREATIVES", "Composing Ad Headlines & Texts"); - campaign.setAdSets(aiService.generateAdCreatives(campaign, analysis)); - } - - // Step 5: AB Test - if (req.isEnableAbTesting() && campaign.getAdSets() != null && !campaign.getAdSets().isEmpty()) { - addStatusHistory(campaign, "CONFIGURING_AB_TEST", "Setting up A/B Test environment"); - abTestingService.createABTest(campaign, campaign.getAdSets().get(0).getAdSetId()); - } - - // Step 6: External Platform Publishing - addStatusHistory(campaign, "PUBLISHING", "Pushing to Social Media Ads APIs"); + // 3. External Platform Publishing + addStatusHistory(campaign, "PUBLISHING", "Pushing validated structure to Social Media Ads APIs"); publishToPlatforms(campaign, userId); - // Step 7: Done + // 4. Finalize if ("failed".equals(campaign.getStatus())) { addStatusHistory(campaign, "FAILED", "Some integrations failed or required credentials missing"); } else { campaign.setStatus("active"); - addStatusHistory(campaign, "ACTIVE", "Campaign successfully processed and active"); + addStatusHistory(campaign, "ACTIVE", "Antigravity Pipeline successfully compiled and pushed the campaign"); } campaign.setUpdatedAt(LocalDateTime.now()); repository.save(campaign); } catch (Exception e) { - log.error("Error during async targeting pipeline", e); + log.error("[Targeting Pipeline] Error during async targeting pipeline", e); failCampaign(campaign, "Pipeline error: " + e.getMessage()); } }