target fix

This commit is contained in:
arys
2026-03-29 20:51:34 +05:00
parent 25e4d496fb
commit e2786d53b0
3 changed files with 517 additions and 109 deletions
@@ -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<CampaignDto> campaigns;
@Valid
@NotNull
private List<AutomationRuleDto> 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<String> specialAdCategories;
@NotBlank
private String buyingType;
@NotNull
@Valid
private BudgetStrategyDto budgetStrategy;
@NotNull
@Valid
private List<AdSetDto> 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<AdDto> 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<Integer> genders;
@Valid
private List<FlexibleSpecDto> flexibleSpec;
@Valid
private ExclusionsDto exclusions;
private List<String> publisherPlatforms;
private List<String> devicePlatforms;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class GeoLocationsDto {
private List<String> countries;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class FlexibleSpecDto {
@Valid
private List<InterestDto> 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<CustomAudienceDto> 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<ConditionDto> 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
}
}
@@ -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<AudienceSegmentDto> 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<TargetingAdSet> generateAdCreatives(TargetingCampaign campaign, MarketingAnalysisV3Document analysis) {
log.info("Generating Ad Creatives for campaign {}", campaign.getId());
List<TargetingAdSet> 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<SocialMediaCredentials> 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<TargetingAd> ads = new ArrayList<>();
if (campaign.getAbTestConfig() != null && campaign.getAbTestConfig().isEnabled()) {
ads.addAll(abTestingService.generateABVariants(context, objective));
} else {
List<TargetingAd> 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<SocialMediaCredentials> credentials) throws JsonProcessingException {
// 1. Business Metrics Context
Map<String, Object> 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<MarketingStrategy.PostCalendarItem> 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<AudienceSegmentDto> analyzeCompetitorAds(String businessNiche, List<String> 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();
}
}
@@ -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<SocialMediaCredentials> 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<TargetingAdSet> 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<TargetingAd> 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<String> 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());
}
}