Files
zarinpal-analytics-backend/internal/growth/opportunities.go

102 lines
2.9 KiB
Go

package growth
import (
"slices"
"strings"
"time"
)
func historicalWins(history []DailyPoint, catalog []Event, before time.Time) []EventEffect {
wins := make([]EventEffect, 0)
for _, event := range catalog {
if !dateBefore(event.EndDate, before) {
continue
}
effect := AnalyzeEventEffect(history, event, catalog)
if effect.Eligible && effect.MaterialWin {
wins = append(wins, effect)
}
}
slices.SortFunc(wins, func(a, b EventEffect) int {
aScore := maxInt64(a.PaidSessionUpliftBPS, a.AmountUpliftBPS)
bScore := maxInt64(b.PaidSessionUpliftBPS, b.AmountUpliftBPS)
if aScore != bScore {
if aScore > bScore {
return -1
}
return 1
}
return strings.Compare(a.EventID, b.EventID)
})
return wins
}
func buildOpportunities(wins []EventEffect, catalog []Event, forecast Forecast, start time.Time, limit int) []Opportunity {
if forecast.Confidence == ConfidenceInsufficientData || limit <= 0 {
return nil
}
eventByID := make(map[string]Event, len(catalog))
for _, event := range catalog {
eventByID[event.ID] = event
}
forecastByDate := make(map[string]ForecastPoint, len(forecast.Points))
for _, point := range forecast.Points {
forecastByDate[dayKey(point.Date)] = point
}
best := make(map[string]Opportunity)
for _, win := range wins {
historical := eventByID[win.EventID]
upcoming := make([]Event, 0)
for _, event := range catalog {
if !dateBefore(event.EndDate, start) {
upcoming = append(upcoming, event)
}
}
for _, match := range MatchUpcomingEvents(historical, upcoming) {
point, ok := forecastByDate[dayKey(match.Event.StartDate)]
if !ok {
continue
}
candidate := Opportunity{
EvidenceID: EvidenceID("opportunity", match.Event.ID+":"+win.EventID), EventID: match.Event.ID,
EventTitleFA: match.Event.TitleFA, Date: normalizeDate(match.Event.StartDate), SimilarityBPS: match.ScoreBPS,
ExpectedSessions: point.PaidSessions, ExpectedAmountRial: point.PaidAmountRial,
HistoricalEvidenceID: win.EvidenceID,
}
if current, exists := best[match.Event.ID]; !exists || candidate.SimilarityBPS > current.SimilarityBPS {
best[match.Event.ID] = candidate
}
}
}
result := make([]Opportunity, 0, len(best))
for _, opportunity := range best {
result = append(result, opportunity)
}
slices.SortFunc(result, func(a, b Opportunity) int {
if a.SimilarityBPS != b.SimilarityBPS {
if a.SimilarityBPS > b.SimilarityBPS {
return -1
}
return 1
}
if comparison := a.Date.Compare(b.Date); comparison != 0 {
return comparison
}
return strings.Compare(a.EventID, b.EventID)
})
if len(result) > limit {
result = result[:limit]
}
return result
}
func filterCatalog(catalog []Event, category string) []Event {
filtered := make([]Event, 0, len(catalog))
for _, event := range catalog {
if category == "" || event.Universal || slices.Contains(event.Categories, category) {
filtered = append(filtered, event)
}
}
return filtered
}