diff --git a/src/main/java/kz/konturai/parser/controller/MarketingController.java b/src/main/java/kz/konturai/parser/controller/MarketingController.java index 40de561..6afb193 100644 --- a/src/main/java/kz/konturai/parser/controller/MarketingController.java +++ b/src/main/java/kz/konturai/parser/controller/MarketingController.java @@ -110,6 +110,20 @@ public class MarketingController { return ResponseEntity.ok(ApiResponse.success("Анализ запущен успешно", response)); } + @PostMapping("/start/v2") + public ResponseEntity startAnalysisV2( + @RequestHeader(value = "Authorization", required = false) String authHeader, + @Valid @RequestBody MarketingAnalysisRequest request) { + + String userId = extractUserIdFromHeader(authHeader); + if (userId == null) { + return unauthorizedResponse(); + } + + MarketingAnalysisResponseV2 response = marketingAnalysisService.generateAnalysisV2(request); + return ResponseEntity.ok(response); + } + @GetMapping("/{analysisId}") public ResponseEntity getAnalysis( @RequestHeader(value = "Authorization", required = false) String authHeader, diff --git a/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java new file mode 100644 index 0000000..bf1a7de --- /dev/null +++ b/src/main/java/kz/konturai/parser/dto/MarketingAnalysisResponseV2.java @@ -0,0 +1,1151 @@ +package kz.konturai.parser.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import java.util.List; +import java.util.Map; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class MarketingAnalysisResponseV2 { + + private String reportTitle; + private Sections sections; + + public MarketingAnalysisResponseV2() { + } + + public MarketingAnalysisResponseV2(String reportTitle, Sections sections) { + this.reportTitle = reportTitle; + this.sections = sections; + } + + public String getReportTitle() { + return reportTitle; + } + + public void setReportTitle(String reportTitle) { + this.reportTitle = reportTitle; + } + + public Sections getSections() { + return sections; + } + + public void setSections(Sections sections) { + this.sections = sections; + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Sections { + private MarketOverview marketOverview; + private TargetAudience targetAudience; + private CompetitiveEnvironment competitiveEnvironment; + private DemandAndBehavior demandAndBehavior; + private CustomerJourney customerJourney; + private AcquisitionChannels acquisitionChannels; + private MarketingEconomics marketingEconomics; + private Bottlenecks bottlenecks; + private Recommendations recommendations; + + public Sections() { + } + + public MarketOverview getMarketOverview() { + return marketOverview; + } + + public void setMarketOverview(MarketOverview marketOverview) { + this.marketOverview = marketOverview; + } + + public TargetAudience getTargetAudience() { + return targetAudience; + } + + public void setTargetAudience(TargetAudience targetAudience) { + this.targetAudience = targetAudience; + } + + public CompetitiveEnvironment getCompetitiveEnvironment() { + return competitiveEnvironment; + } + + public void setCompetitiveEnvironment(CompetitiveEnvironment competitiveEnvironment) { + this.competitiveEnvironment = competitiveEnvironment; + } + + public DemandAndBehavior getDemandAndBehavior() { + return demandAndBehavior; + } + + public void setDemandAndBehavior(DemandAndBehavior demandAndBehavior) { + this.demandAndBehavior = demandAndBehavior; + } + + public CustomerJourney getCustomerJourney() { + return customerJourney; + } + + public void setCustomerJourney(CustomerJourney customerJourney) { + this.customerJourney = customerJourney; + } + + public AcquisitionChannels getAcquisitionChannels() { + return acquisitionChannels; + } + + public void setAcquisitionChannels(AcquisitionChannels acquisitionChannels) { + this.acquisitionChannels = acquisitionChannels; + } + + public MarketingEconomics getMarketingEconomics() { + return marketingEconomics; + } + + public void setMarketingEconomics(MarketingEconomics marketingEconomics) { + this.marketingEconomics = marketingEconomics; + } + + public Bottlenecks getBottlenecks() { + return bottlenecks; + } + + public void setBottlenecks(Bottlenecks bottlenecks) { + this.bottlenecks = bottlenecks; + } + + public Recommendations getRecommendations() { + return recommendations; + } + + public void setRecommendations(Recommendations recommendations) { + this.recommendations = recommendations; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MarketOverview { + private String title; + private MarketOverviewMetrics metrics; + private MonthlyOnlineIndex monthlyOnlineIndex; + private String geography; + private String conclusion; + + public MarketOverview() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public MarketOverviewMetrics getMetrics() { + return metrics; + } + + public void setMetrics(MarketOverviewMetrics metrics) { + this.metrics = metrics; + } + + public MonthlyOnlineIndex getMonthlyOnlineIndex() { + return monthlyOnlineIndex; + } + + public void setMonthlyOnlineIndex(MonthlyOnlineIndex monthlyOnlineIndex) { + this.monthlyOnlineIndex = monthlyOnlineIndex; + } + + public String getGeography() { + return geography; + } + + public void setGeography(String geography) { + this.geography = geography; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MarketOverviewMetrics { + private String onlineDemandIndex; + private String onlinePotential; + private String demandGrowthRateYoy; + private String seasonalityCoefficientSpring; + private String seasonalityRange; + + public MarketOverviewMetrics() { + } + + public String getOnlineDemandIndex() { + return onlineDemandIndex; + } + + public void setOnlineDemandIndex(String onlineDemandIndex) { + this.onlineDemandIndex = onlineDemandIndex; + } + + public String getOnlinePotential() { + return onlinePotential; + } + + public void setOnlinePotential(String onlinePotential) { + this.onlinePotential = onlinePotential; + } + + public String getDemandGrowthRateYoy() { + return demandGrowthRateYoy; + } + + public void setDemandGrowthRateYoy(String demandGrowthRateYoy) { + this.demandGrowthRateYoy = demandGrowthRateYoy; + } + + public String getSeasonalityCoefficientSpring() { + return seasonalityCoefficientSpring; + } + + public void setSeasonalityCoefficientSpring(String seasonalityCoefficientSpring) { + this.seasonalityCoefficientSpring = seasonalityCoefficientSpring; + } + + public String getSeasonalityRange() { + return seasonalityRange; + } + + public void setSeasonalityRange(String seasonalityRange) { + this.seasonalityRange = seasonalityRange; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MonthlyOnlineIndex { + private Double march; + private Double april; + private Double may; + + public MonthlyOnlineIndex() { + } + + public Double getMarch() { + return march; + } + + public void setMarch(Double march) { + this.march = march; + } + + public Double getApril() { + return april; + } + + public void setApril(Double april) { + this.april = april; + } + + public Double getMay() { + return may; + } + + public void setMay(Double may) { + this.may = may; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class TargetAudience { + private String title; + private TargetAudienceMetrics metrics; + private List segments; + private Map ageStructure; + private String conclusion; + + public TargetAudience() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public TargetAudienceMetrics getMetrics() { + return metrics; + } + + public void setMetrics(TargetAudienceMetrics metrics) { + this.metrics = metrics; + } + + public List getSegments() { + return segments; + } + + public void setSegments(List segments) { + this.segments = segments; + } + + public Map getAgeStructure() { + return ageStructure; + } + + public void setAgeStructure(Map ageStructure) { + this.ageStructure = ageStructure; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class TargetAudienceMetrics { + private String totalAudienceSize; + private String leadingSegmentShare; + private String aboveAverageIncomeShare; + + public TargetAudienceMetrics() { + } + + public String getTotalAudienceSize() { + return totalAudienceSize; + } + + public void setTotalAudienceSize(String totalAudienceSize) { + this.totalAudienceSize = totalAudienceSize; + } + + public String getLeadingSegmentShare() { + return leadingSegmentShare; + } + + public void setLeadingSegmentShare(String leadingSegmentShare) { + this.leadingSegmentShare = leadingSegmentShare; + } + + public String getAboveAverageIncomeShare() { + return aboveAverageIncomeShare; + } + + public void setAboveAverageIncomeShare(String aboveAverageIncomeShare) { + this.aboveAverageIncomeShare = aboveAverageIncomeShare; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AudienceSegment { + private String name; + private String share; + + public AudienceSegment() { + } + + public AudienceSegment(String name, String share) { + this.name = name; + this.share = share; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getShare() { + return share; + } + + public void setShare(String share) { + this.share = share; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CompetitiveEnvironment { + private String title; + private CompetitiveEnvironmentMetrics metrics; + private Map digitalVisibilityBenchmarks; + private Map> competitorStrengths; + private String conclusion; + + public CompetitiveEnvironment() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public CompetitiveEnvironmentMetrics getMetrics() { + return metrics; + } + + public void setMetrics(CompetitiveEnvironmentMetrics metrics) { + this.metrics = metrics; + } + + public Map getDigitalVisibilityBenchmarks() { + return digitalVisibilityBenchmarks; + } + + public void setDigitalVisibilityBenchmarks(Map digitalVisibilityBenchmarks) { + this.digitalVisibilityBenchmarks = digitalVisibilityBenchmarks; + } + + public Map> getCompetitorStrengths() { + return competitorStrengths; + } + + public void setCompetitorStrengths(Map> competitorStrengths) { + this.competitorStrengths = competitorStrengths; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CompetitiveEnvironmentMetrics { + private String marketShareTraffic; + private String visibilityRank; + private String competitorShareInContextAds; + private String benchmarkAstana; + + public CompetitiveEnvironmentMetrics() { + } + + public String getMarketShareTraffic() { + return marketShareTraffic; + } + + public void setMarketShareTraffic(String marketShareTraffic) { + this.marketShareTraffic = marketShareTraffic; + } + + public String getVisibilityRank() { + return visibilityRank; + } + + public void setVisibilityRank(String visibilityRank) { + this.visibilityRank = visibilityRank; + } + + public String getCompetitorShareInContextAds() { + return competitorShareInContextAds; + } + + public void setCompetitorShareInContextAds(String competitorShareInContextAds) { + this.competitorShareInContextAds = competitorShareInContextAds; + } + + public String getBenchmarkAstana() { + return benchmarkAstana; + } + + public void setBenchmarkAstana(String benchmarkAstana) { + this.benchmarkAstana = benchmarkAstana; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class DemandAndBehavior { + private String title; + private DemandAndBehaviorMetrics metrics; + private Map searchQueryTypes; + private Map decisionFactors; + private Map reasonsForRefusal; + private String conclusion; + + public DemandAndBehavior() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public DemandAndBehaviorMetrics getMetrics() { + return metrics; + } + + public void setMetrics(DemandAndBehaviorMetrics metrics) { + this.metrics = metrics; + } + + public Map getSearchQueryTypes() { + return searchQueryTypes; + } + + public void setSearchQueryTypes(Map searchQueryTypes) { + this.searchQueryTypes = searchQueryTypes; + } + + public Map getDecisionFactors() { + return decisionFactors; + } + + public void setDecisionFactors(Map decisionFactors) { + this.decisionFactors = decisionFactors; + } + + public Map getReasonsForRefusal() { + return reasonsForRefusal; + } + + public void setReasonsForRefusal(Map reasonsForRefusal) { + this.reasonsForRefusal = reasonsForRefusal; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class DemandAndBehaviorMetrics { + private String searchVolumeGrowth; + private String interactionsBeforePurchase; + private String repeatRefusalProbability; + + public DemandAndBehaviorMetrics() { + } + + public String getSearchVolumeGrowth() { + return searchVolumeGrowth; + } + + public void setSearchVolumeGrowth(String searchVolumeGrowth) { + this.searchVolumeGrowth = searchVolumeGrowth; + } + + public String getInteractionsBeforePurchase() { + return interactionsBeforePurchase; + } + + public void setInteractionsBeforePurchase(String interactionsBeforePurchase) { + this.interactionsBeforePurchase = interactionsBeforePurchase; + } + + public String getRepeatRefusalProbability() { + return repeatRefusalProbability; + } + + public void setRepeatRefusalProbability(String repeatRefusalProbability) { + this.repeatRefusalProbability = repeatRefusalProbability; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CustomerJourney { + private String title; + private FunnelMetrics funnelMetrics; + private ConversionRates conversionRates; + private FunnelBottlenecks funnelBottlenecks; + private String conclusion; + + public CustomerJourney() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public FunnelMetrics getFunnelMetrics() { + return funnelMetrics; + } + + public void setFunnelMetrics(FunnelMetrics funnelMetrics) { + this.funnelMetrics = funnelMetrics; + } + + public ConversionRates getConversionRates() { + return conversionRates; + } + + public void setConversionRates(ConversionRates conversionRates) { + this.conversionRates = conversionRates; + } + + public FunnelBottlenecks getFunnelBottlenecks() { + return funnelBottlenecks; + } + + public void setFunnelBottlenecks(FunnelBottlenecks funnelBottlenecks) { + this.funnelBottlenecks = funnelBottlenecks; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class FunnelMetrics { + private String adTraffic; + private String siteVisitors; + private Integer leads; + private Integer bookings; + private Integer payments; + + public FunnelMetrics() { + } + + public String getAdTraffic() { + return adTraffic; + } + + public void setAdTraffic(String adTraffic) { + this.adTraffic = adTraffic; + } + + public String getSiteVisitors() { + return siteVisitors; + } + + public void setSiteVisitors(String siteVisitors) { + this.siteVisitors = siteVisitors; + } + + public Integer getLeads() { + return leads; + } + + public void setLeads(Integer leads) { + this.leads = leads; + } + + public Integer getBookings() { + return bookings; + } + + public void setBookings(Integer bookings) { + this.bookings = bookings; + } + + public Integer getPayments() { + return payments; + } + + public void setPayments(Integer payments) { + this.payments = payments; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class ConversionRates { + private String leadToBooking; + private String bookingToPayment; + private String leadToPaymentTotal; + + public ConversionRates() { + } + + public String getLeadToBooking() { + return leadToBooking; + } + + public void setLeadToBooking(String leadToBooking) { + this.leadToBooking = leadToBooking; + } + + public String getBookingToPayment() { + return bookingToPayment; + } + + public void setBookingToPayment(String bookingToPayment) { + this.bookingToPayment = bookingToPayment; + } + + public String getLeadToPaymentTotal() { + return leadToPaymentTotal; + } + + public void setLeadToPaymentTotal(String leadToPaymentTotal) { + this.leadToPaymentTotal = leadToPaymentTotal; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class FunnelBottlenecks { + private String leadToBooking; + private String bookingToPayment; + + public FunnelBottlenecks() { + } + + public String getLeadToBooking() { + return leadToBooking; + } + + public void setLeadToBooking(String leadToBooking) { + this.leadToBooking = leadToBooking; + } + + public String getBookingToPayment() { + return bookingToPayment; + } + + public void setBookingToPayment(String bookingToPayment) { + this.bookingToPayment = bookingToPayment; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AcquisitionChannels { + private String title; + private AcquisitionChannelsMetrics metrics; + private Map channelShares; + private Map channelEfficiency; + private String conclusion; + + public AcquisitionChannels() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public AcquisitionChannelsMetrics getMetrics() { + return metrics; + } + + public void setMetrics(AcquisitionChannelsMetrics metrics) { + this.metrics = metrics; + } + + public Map getChannelShares() { + return channelShares; + } + + public void setChannelShares(Map channelShares) { + this.channelShares = channelShares; + } + + public Map getChannelEfficiency() { + return channelEfficiency; + } + + public void setChannelEfficiency(Map channelEfficiency) { + this.channelEfficiency = channelEfficiency; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class AcquisitionChannelsMetrics { + private String searchTrafficShare; + private String averageLeadCost; + private String growthPotential; + + public AcquisitionChannelsMetrics() { + } + + public String getSearchTrafficShare() { + return searchTrafficShare; + } + + public void setSearchTrafficShare(String searchTrafficShare) { + this.searchTrafficShare = searchTrafficShare; + } + + public String getAverageLeadCost() { + return averageLeadCost; + } + + public void setAverageLeadCost(String averageLeadCost) { + this.averageLeadCost = averageLeadCost; + } + + public String getGrowthPotential() { + return growthPotential; + } + + public void setGrowthPotential(String growthPotential) { + this.growthPotential = growthPotential; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class ChannelEfficiency { + private String cost; + private String status; + + public ChannelEfficiency() { + } + + public ChannelEfficiency(String cost, String status) { + this.cost = cost; + this.status = status; + } + + public String getCost() { + return cost; + } + + public void setCost(String cost) { + this.cost = cost; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MarketingEconomics { + private String title; + private MarketingEconomicsMetrics metrics; + private FinancialPerformance financialPerformance; + private String conclusion; + + public MarketingEconomics() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public MarketingEconomicsMetrics getMetrics() { + return metrics; + } + + public void setMetrics(MarketingEconomicsMetrics metrics) { + this.metrics = metrics; + } + + public FinancialPerformance getFinancialPerformance() { + return financialPerformance; + } + + public void setFinancialPerformance(FinancialPerformance financialPerformance) { + this.financialPerformance = financialPerformance; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MarketingEconomicsMetrics { + private String customerAcquisitionCostCac; + private String repeatSalesConversion; + private String roi; + + public MarketingEconomicsMetrics() { + } + + public String getCustomerAcquisitionCostCac() { + return customerAcquisitionCostCac; + } + + public void setCustomerAcquisitionCostCac(String customerAcquisitionCostCac) { + this.customerAcquisitionCostCac = customerAcquisitionCostCac; + } + + public String getRepeatSalesConversion() { + return repeatSalesConversion; + } + + public void setRepeatSalesConversion(String repeatSalesConversion) { + this.repeatSalesConversion = repeatSalesConversion; + } + + public String getRoi() { + return roi; + } + + public void setRoi(String roi) { + this.roi = roi; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class FinancialPerformance { + private MonthYearValue marketingExpenses; + private MonthYearValue clients; + private MonthYearValue cac; + private MonthYearValue revenueFromClient; + private MonthYearValue profitFromClient; + + public FinancialPerformance() { + } + + public MonthYearValue getMarketingExpenses() { + return marketingExpenses; + } + + public void setMarketingExpenses(MonthYearValue marketingExpenses) { + this.marketingExpenses = marketingExpenses; + } + + public MonthYearValue getClients() { + return clients; + } + + public void setClients(MonthYearValue clients) { + this.clients = clients; + } + + public MonthYearValue getCac() { + return cac; + } + + public void setCac(MonthYearValue cac) { + this.cac = cac; + } + + public MonthYearValue getRevenueFromClient() { + return revenueFromClient; + } + + public void setRevenueFromClient(MonthYearValue revenueFromClient) { + this.revenueFromClient = revenueFromClient; + } + + public MonthYearValue getProfitFromClient() { + return profitFromClient; + } + + public void setProfitFromClient(MonthYearValue profitFromClient) { + this.profitFromClient = profitFromClient; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class MonthYearValue { + private String month; + private String year; + + public MonthYearValue() { + } + + public MonthYearValue(String month, String year) { + this.month = month; + this.year = year; + } + + public String getMonth() { + return month; + } + + public void setMonth(String month) { + this.month = month; + } + + public String getYear() { + return year; + } + + public void setYear(String year) { + this.year = year; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Bottlenecks { + private String title; + private Map statistics; + private BottlenecksDetails details; + private String conclusion; + + public Bottlenecks() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Map getStatistics() { + return statistics; + } + + public void setStatistics(Map statistics) { + this.statistics = statistics; + } + + public BottlenecksDetails getDetails() { + return details; + } + + public void setDetails(BottlenecksDetails details) { + this.details = details; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class BottlenecksDetails { + private List landing; + private List leadManagement; + private List retention; + + public BottlenecksDetails() { + } + + public List getLanding() { + return landing; + } + + public void setLanding(List landing) { + this.landing = landing; + } + + public List getLeadManagement() { + return leadManagement; + } + + public void setLeadManagement(List leadManagement) { + this.leadManagement = leadManagement; + } + + public List getRetention() { + return retention; + } + + public void setRetention(List retention) { + this.retention = retention; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class Recommendations { + private String title; + private List corePillars; + private String conclusion; + + public Recommendations() { + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getCorePillars() { + return corePillars; + } + + public void setCorePillars(List corePillars) { + this.corePillars = corePillars; + } + + public String getConclusion() { + return conclusion; + } + + public void setConclusion(String conclusion) { + this.conclusion = conclusion; + } + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public static class CorePillar { + private String strategy; + private String action; + + public CorePillar() { + } + + public CorePillar(String strategy, String action) { + this.strategy = strategy; + this.action = action; + } + + public String getStrategy() { + return strategy; + } + + public void setStrategy(String strategy) { + this.strategy = strategy; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + } +} + + diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index f07993f..9a2cedf 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -2,6 +2,7 @@ package kz.konturai.parser.service; import kz.konturai.parser.dto.MarketingAnalysisRequest; import kz.konturai.parser.dto.MarketingAnalysisResult; +import kz.konturai.parser.dto.MarketingAnalysisResponseV2; import kz.konturai.parser.dto.SerperSearchItem; import kz.konturai.parser.dto.SerperSearchResult; import kz.konturai.parser.dto.StatusHistoryEntry; @@ -633,6 +634,295 @@ public class MarketingAnalysisService { return List.of(analysis); } + /** + * V2 endpoint payload for frontend integration testing. + * Generates a strict JSON response via OpenAI, grounded on Serper web evidence. + */ + public MarketingAnalysisResponseV2 generateAnalysisV2(MarketingAnalysisRequest request) { + try { + // 1) Gather web evidence + Map researchPack = buildResearchPack(request); + + // 2) Build strict schema instructions + provide an example JSON structure + String exampleJson = objectMapper.writeValueAsString(buildAnalysisV2ExampleForPrompt()); + + String systemPrompt = "" + + "Ты — партнер Big Four (управленческий консалтинг) и senior marketing analyst.\n" + + "Пиши уверенно, структурно, с причинно-следственной логикой, без «воды».\n" + + "КРИТИЧЕСКИ ВАЖНО: Твой ответ должен быть ТОЛЬКО валидный JSON, без markdown, без ```.\n"; + + String instruction = "" + + "Сгенерируй один JSON-объект, который СТРОГО соответствует схеме MarketingAnalysisResponseV2.\n" + + "Обязательные правила:\n" + + "1) Верни ТОЛЬКО JSON (без текста до/после).\n" + + "2) Заполни ВСЕ вложенные поля: sections.market_overview, target_audience, competitive_environment, " + + "demand_and_behavior, customer_journey, acquisition_channels, marketing_economics, bottlenecks, recommendations.\n" + + "3) Используй реальные цифры/темпы роста/проценты из evidence (researchPack). Если конкретных цифр нет — сделай Analytical Extrapolation, " + + "но НИКОГДА не пиши «данных нет/не найдено/недоступно».\n" + + "4) Соблюдай типы:\n" + + " - проценты/строки формата \"76/100\", \"+27%\", \"1.34x\" => string\n" + + " - monthly_online_index.* => number (double)\n" + + " - funnel_metrics.leads/bookings/payments => integer\n" + + "5) Пиши на русском языке.\n\n" + + "Ниже пример правильной структуры (пример для ориентира; цифры должны быть основаны на evidence):\n" + + exampleJson; + + String contextJson = objectMapper.writeValueAsString(Map.of( + "request", request, + "evidence", researchPack)); + + // 3) Call OpenAI (sleep to reduce 429 rate-limit bursts) + try { + Thread.sleep(2000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + + String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel( + contextJson, + instruction, + "ru", + textModelName, + systemPrompt, + 15000, + 180000L); + + if (aiResponse == null || aiResponse.isBlank()) { + throw new IllegalStateException("OpenAI returned empty response for analysis v2"); + } + + // 4) Clean + parse + String cleaned = cleanJsonFromMarkdown(aiResponse); + if (cleaned != null) { + cleaned = cleaned.replace("\uFEFF", "").trim(); + } + return objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class); + } catch (Exception e) { + logger.error("generateAnalysisV2: Failed to generate/parse v2 response: {}", e.getMessage(), e); + throw new RuntimeException("Не удалось сгенерировать marketing analysis v2", e); + } + } + + /** + * Example payload used ONLY for prompting the AI with the expected JSON + * schema/shape. + * This is not returned to the client. + */ + private MarketingAnalysisResponseV2 buildAnalysisV2ExampleForPrompt() { + MarketingAnalysisResponseV2 response = new MarketingAnalysisResponseV2(); + response.setReportTitle("Маркетинговый анализ для интернет-продвижения"); + + MarketingAnalysisResponseV2.Sections sections = new MarketingAnalysisResponseV2.Sections(); + + // market_overview + MarketingAnalysisResponseV2.MarketOverview marketOverview = new MarketingAnalysisResponseV2.MarketOverview(); + marketOverview.setTitle("Обзор рынка"); + MarketingAnalysisResponseV2.MarketOverviewMetrics moMetrics = new MarketingAnalysisResponseV2.MarketOverviewMetrics(); + moMetrics.setOnlineDemandIndex("76/100"); + moMetrics.setOnlinePotential("Высокий"); + moMetrics.setDemandGrowthRateYoy("+27%"); + moMetrics.setSeasonalityCoefficientSpring("1.34x"); + moMetrics.setSeasonalityRange("1.29 - 1.38"); + marketOverview.setMetrics(moMetrics); + MarketingAnalysisResponseV2.MonthlyOnlineIndex monthlyOnlineIndex = new MarketingAnalysisResponseV2.MonthlyOnlineIndex(); + monthlyOnlineIndex.setMarch(1.32); + monthlyOnlineIndex.setApril(1.34); + monthlyOnlineIndex.setMay(1.38); + marketOverview.setMonthlyOnlineIndex(monthlyOnlineIndex); + marketOverview.setGeography("Казахстан (анализ по регионам представлен на тепловой карте)"); + marketOverview.setConclusion( + "Онлайн-спрос показывает устойчивый рост в течение года, весной достигая пика. Интернет-продвижение даст максимальный эффект."); + sections.setMarketOverview(marketOverview); + + // target_audience + MarketingAnalysisResponseV2.TargetAudience targetAudience = new MarketingAnalysisResponseV2.TargetAudience(); + targetAudience.setTitle("Целевая аудитория"); + MarketingAnalysisResponseV2.TargetAudienceMetrics taMetrics = new MarketingAnalysisResponseV2.TargetAudienceMetrics(); + taMetrics.setTotalAudienceSize("1.8 млн человек"); + taMetrics.setLeadingSegmentShare("29%"); + taMetrics.setAboveAverageIncomeShare("35%"); + targetAudience.setMetrics(taMetrics); + targetAudience.setSegments(List.of( + new MarketingAnalysisResponseV2.AudienceSegment("Женщины, 25–34 года", "29%"), + new MarketingAnalysisResponseV2.AudienceSegment("Мужчины, 25–44 года", "24%"), + new MarketingAnalysisResponseV2.AudienceSegment("Доход выше среднего", "35%"))); + Map ageStructure = new LinkedHashMap<>(); + ageStructure.put("18-24", "10%"); + ageStructure.put("25-34", "29%"); + ageStructure.put("35-44", "Высокая доля (визуально на графике)"); + ageStructure.put("45-54", "15-24%"); + ageStructure.put("55_plus", "Низкая доля"); + targetAudience.setAgeStructure(ageStructure); + targetAudience.setConclusion( + "Основную ценность представляют мужчины и женщины 25–44 лет с доходом выше среднего (до 35% аудитории)."); + sections.setTargetAudience(targetAudience); + + // competitive_environment + MarketingAnalysisResponseV2.CompetitiveEnvironment competitiveEnvironment = new MarketingAnalysisResponseV2.CompetitiveEnvironment(); + competitiveEnvironment.setTitle("Конкурентная среда"); + MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics ceMetrics = new MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics(); + ceMetrics.setMarketShareTraffic("16%"); + ceMetrics.setVisibilityRank("#3"); + ceMetrics.setCompetitorShareInContextAds("27%"); + ceMetrics.setBenchmarkAstana("13-25%"); + competitiveEnvironment.setMetrics(ceMetrics); + Map digitalVisibilityBenchmarks = new LinkedHashMap<>(); + digitalVisibilityBenchmarks.put("competitor_a", 1050); + digitalVisibilityBenchmarks.put("your_business", 600); + digitalVisibilityBenchmarks.put("lead", 300); + digitalVisibilityBenchmarks.put("payment", 450); + digitalVisibilityBenchmarks.put("others", 680); + competitiveEnvironment.setDigitalVisibilityBenchmarks(digitalVisibilityBenchmarks); + Map> competitorStrengths = new LinkedHashMap<>(); + competitorStrengths.put("your_business", List.of("Цена", "Оффер")); + competitorStrengths.put("competitor_a", List.of("Отзывы", "Рекламный бюджет")); + competitorStrengths.put("competitor_b", List.of("Контент", "Соцсети", "Экспириенс")); + competitorStrengths.put("competitor_c", List.of("Контент", "Сайт")); + competitiveEnvironment.setCompetitorStrengths(competitorStrengths); + competitiveEnvironment.setConclusion( + "Ваш бизнес занимает 3-е место по видимости. Основные преимущества конкурентов — соцдоказательства и контент. Ценовая борьба не является оптимальной."); + sections.setCompetitiveEnvironment(competitiveEnvironment); + + // demand_and_behavior + MarketingAnalysisResponseV2.DemandAndBehavior demandAndBehavior = new MarketingAnalysisResponseV2.DemandAndBehavior(); + demandAndBehavior.setTitle("Спрос и поведение"); + MarketingAnalysisResponseV2.DemandAndBehaviorMetrics dabMetrics = new MarketingAnalysisResponseV2.DemandAndBehaviorMetrics(); + dabMetrics.setSearchVolumeGrowth("+62%"); + dabMetrics.setInteractionsBeforePurchase("3-4"); + dabMetrics.setRepeatRefusalProbability("54%"); + demandAndBehavior.setMetrics(dabMetrics); + Map searchQueryTypes = new LinkedHashMap<>(); + searchQueryTypes.put("direct_interest_catalog", "52%"); + searchQueryTypes.put("question_based_articles", "28%"); + searchQueryTypes.put("competitors_reviews", "13%"); + searchQueryTypes.put("problem_solving_hot_promo", "7%"); + demandAndBehavior.setSearchQueryTypes(searchQueryTypes); + Map decisionFactors = new LinkedHashMap<>(); + decisionFactors.put("promo_discount", "43%"); + decisionFactors.put("review_recommendation", "36%"); + decisionFactors.put("warranty_quality", "33%"); + decisionFactors.put("convenience_service", "25%"); + decisionFactors.put("availability", "21%"); + decisionFactors.put("price", "19%"); + demandAndBehavior.setDecisionFactors(decisionFactors); + Map reasonsForRefusal = new LinkedHashMap<>(); + reasonsForRefusal.put("high_price", "37%"); + reasonsForRefusal.put("no_reviews_social_proof", "34%"); + reasonsForRefusal.put("choice_difficulty_unclear_offer", "29%"); + demandAndBehavior.setReasonsForRefusal(reasonsForRefusal); + demandAndBehavior.setConclusion( + "Основной трафик приходит из поиска. Для отклика ключевую роль играют акции и отзывы."); + sections.setDemandAndBehavior(demandAndBehavior); + + // customer_journey + MarketingAnalysisResponseV2.CustomerJourney customerJourney = new MarketingAnalysisResponseV2.CustomerJourney(); + customerJourney.setTitle("Путь клиента"); + MarketingAnalysisResponseV2.FunnelMetrics funnelMetrics = new MarketingAnalysisResponseV2.FunnelMetrics(); + funnelMetrics.setAdTraffic("Реклама"); + funnelMetrics.setSiteVisitors("Сайт / Лендинг"); + funnelMetrics.setLeads(5600); + funnelMetrics.setBookings(1200); + funnelMetrics.setPayments(680); + customerJourney.setFunnelMetrics(funnelMetrics); + MarketingAnalysisResponseV2.ConversionRates conversionRates = new MarketingAnalysisResponseV2.ConversionRates(); + conversionRates.setLeadToBooking("21%"); + conversionRates.setBookingToPayment("57%"); + conversionRates.setLeadToPaymentTotal("12%"); + customerJourney.setConversionRates(conversionRates); + MarketingAnalysisResponseV2.FunnelBottlenecks funnelBottlenecks = new MarketingAnalysisResponseV2.FunnelBottlenecks(); + funnelBottlenecks.setLeadToBooking("Медленная скорость ответа"); + funnelBottlenecks.setBookingToPayment("Недостаточно касаний / напоминаний"); + customerJourney.setFunnelBottlenecks(funnelBottlenecks); + customerJourney.setConclusion( + "Основные потери происходят на этапе лид -> запись (конверсия 21%) из-за медленных ответов менеджеров."); + sections.setCustomerJourney(customerJourney); + + // acquisition_channels + MarketingAnalysisResponseV2.AcquisitionChannels acquisitionChannels = new MarketingAnalysisResponseV2.AcquisitionChannels(); + acquisitionChannels.setTitle("Каналы привлечения"); + MarketingAnalysisResponseV2.AcquisitionChannelsMetrics acMetrics = new MarketingAnalysisResponseV2.AcquisitionChannelsMetrics(); + acMetrics.setSearchTrafficShare("48%"); + acMetrics.setAverageLeadCost("37 P"); + acMetrics.setGrowthPotential("+21%"); + acquisitionChannels.setMetrics(acMetrics); + Map channelShares = new LinkedHashMap<>(); + channelShares.put("seo", "48%"); + channelShares.put("yandex_direct", "31%"); + channelShares.put("retargeting", "13%"); + channelShares.put("vkontakte", "6%"); + acquisitionChannels.setChannelShares(channelShares); + Map channelEfficiency = new LinkedHashMap<>(); + channelEfficiency.put("seo", new MarketingAnalysisResponseV2.ChannelEfficiency("25 P", "Эффективен")); + channelEfficiency.put("yandex", new MarketingAnalysisResponseV2.ChannelEfficiency("45 P", "Эффективен")); + channelEfficiency.put("retargeting", + new MarketingAnalysisResponseV2.ChannelEfficiency("40 P", "Удовлетворительно")); + channelEfficiency.put("vkontakte", new MarketingAnalysisResponseV2.ChannelEfficiency("57 P", "Завышено")); + channelEfficiency.put("youtube", new MarketingAnalysisResponseV2.ChannelEfficiency("63 P", "Завышено")); + channelEfficiency.put("instagram", new MarketingAnalysisResponseV2.ChannelEfficiency("77 P", "Завышено")); + acquisitionChannels.setChannelEfficiency(channelEfficiency); + acquisitionChannels.setConclusion( + "Основной источник лидов — SEO. VK, Youtube и Instagram требуют оптимизации из-за завышенной стоимости лида."); + sections.setAcquisitionChannels(acquisitionChannels); + + // marketing_economics + MarketingAnalysisResponseV2.MarketingEconomics marketingEconomics = new MarketingAnalysisResponseV2.MarketingEconomics(); + marketingEconomics.setTitle("Экономика маркетинга"); + MarketingAnalysisResponseV2.MarketingEconomicsMetrics meMetrics = new MarketingAnalysisResponseV2.MarketingEconomicsMetrics(); + meMetrics.setCustomerAcquisitionCostCac("2900 P"); + meMetrics.setRepeatSalesConversion("32%"); + meMetrics.setRoi("3.5x"); + marketingEconomics.setMetrics(meMetrics); + MarketingAnalysisResponseV2.FinancialPerformance financialPerformance = new MarketingAnalysisResponseV2.FinancialPerformance(); + financialPerformance + .setMarketingExpenses(new MarketingAnalysisResponseV2.MonthYearValue("1.97 млн", "23.6 млн")); + financialPerformance.setClients(new MarketingAnalysisResponseV2.MonthYearValue("680", "8160")); + financialPerformance.setCac(new MarketingAnalysisResponseV2.MonthYearValue("2900 P", "2900 P")); + financialPerformance + .setRevenueFromClient(new MarketingAnalysisResponseV2.MonthYearValue("10.200 P", "23.040 P")); + financialPerformance.setProfitFromClient(new MarketingAnalysisResponseV2.MonthYearValue("7.300 P", "16.720 P")); + marketingEconomics.setFinancialPerformance(financialPerformance); + marketingEconomics.setConclusion( + "Экономика digital-маркетинга эффективна: возврат инвестиций 3.5х. Точки роста — повторные продажи и удержание."); + sections.setMarketingEconomics(marketingEconomics); + + // bottlenecks + MarketingAnalysisResponseV2.Bottlenecks bottlenecks = new MarketingAnalysisResponseV2.Bottlenecks(); + bottlenecks.setTitle("Проблемы и узкие места"); + Map statistics = new LinkedHashMap<>(); + statistics.put("landing_page_issues", "51%"); + statistics.put("slow_manager_responses", "60%"); + statistics.put("repeat_interaction_refusal", "36%"); + bottlenecks.setStatistics(statistics); + MarketingAnalysisResponseV2.BottlenecksDetails details = new MarketingAnalysisResponseV2.BottlenecksDetails(); + details.setLanding(List.of("Низкая конверсия (2%)", "Проблемы с формой записи", "Уход посетителей")); + details.setLeadManagement( + List.of("Медленные ответы (30-40+ мин)", "Недостаток напоминаний", "Пропущенные лиды")); + details.setRetention(List.of("Недостаток повторных предложений", "Нет прогрева", "Слабая работа с CRM")); + bottlenecks.setDetails(details); + bottlenecks.setConclusion( + "Теряются десятки процентов лидов из-за медленных ответов менеджеров и слабой ретаргетинговой работы."); + sections.setBottlenecks(bottlenecks); + + // recommendations + MarketingAnalysisResponseV2.Recommendations recommendations = new MarketingAnalysisResponseV2.Recommendations(); + recommendations.setTitle("Рекомендации (итоги)"); + recommendations.setCorePillars(List.of( + new MarketingAnalysisResponseV2.CorePillar("Фокус на эффективных каналах", + "Перераспределение бюджета на каналы с устойчивым спросом (Яндекс/SEO)"), + new MarketingAnalysisResponseV2.CorePillar("Увеличение повторных касаний", + "Усиление удерживающих кампаний и серий касаний с клиентами"), + new MarketingAnalysisResponseV2.CorePillar("Приоритет высокоценных аудиторий", + "Смещение акцента на платёжеспособные сегменты и B2B"), + new MarketingAnalysisResponseV2.CorePillar("Рост эффективности трафика", + "Снижение зависимости от дорогих источников и оптимизация каналов"))); + recommendations.setConclusion( + "Рекомендации фокусируют усилия на эффективности интернет-продвижения и служат базой для следующих шагов."); + sections.setRecommendations(recommendations); + + response.setSections(sections); + return response; + } + private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) { if (analysis.getStatusHistory() == null) { analysis.setStatusHistory(new ArrayList<>());