package growth import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "time" ) type FingerprintInput struct { MerchantID int64 `json:"merchantId"` TerminalID string `json:"terminalId,omitempty"` DatasetWatermark string `json:"datasetWatermark"` ForecastStart time.Time `json:"forecastStart"` HorizonDays int `json:"horizonDays"` CatalogVersion string `json:"catalogVersion"` AnalyticsVersion string `json:"analyticsVersion"` PromptVersion string `json:"promptVersion"` SchemaVersion string `json:"schemaVersion"` Model string `json:"model"` } func Fingerprint(input FingerprintInput) string { payload, err := json.Marshal(struct { FingerprintInput ForecastDate string `json:"forecastDate"` }{FingerprintInput: FingerprintInput{ MerchantID: input.MerchantID, TerminalID: input.TerminalID, DatasetWatermark: input.DatasetWatermark, HorizonDays: input.HorizonDays, CatalogVersion: input.CatalogVersion, AnalyticsVersion: input.AnalyticsVersion, PromptVersion: input.PromptVersion, SchemaVersion: input.SchemaVersion, Model: input.Model, }, ForecastDate: input.ForecastStart.In(tehranLocation()).Format(time.DateOnly)}) if err != nil { return "" } digest := sha256.Sum256(payload) return hex.EncodeToString(digest[:]) } type ReportInput struct { History []DailyPoint `json:"history"` Catalog []Event `json:"catalog"` ForecastStart time.Time `json:"forecastStart"` HorizonDays int `json:"horizonDays"` Category string `json:"category,omitempty"` Peer *PeerInput `json:"peer,omitempty"` } type DeterministicReport struct { Forecast Forecast `json:"forecast"` HistoricalWins []EventEffect `json:"historicalWins"` Opportunities []Opportunity `json:"opportunities"` Risks []Risk `json:"risks"` Peer *PeerResult `json:"peer,omitempty"` FallbackBriefs []CampaignBrief `json:"fallbackBriefs"` } func BuildDeterministicReport(input ReportInput) DeterministicReport { catalog := filterCatalog(input.Catalog, input.Category) forecast := BuildForecast(input.History, catalog, input.ForecastStart, input.HorizonDays) wins := historicalWins(input.History, catalog, input.ForecastStart) opportunities := buildOpportunities(wins, catalog, forecast, input.ForecastStart, 5) risks := DetectRisks(forecast, 3) bundle := EvidenceBundle{Opportunities: opportunities, Risks: risks} report := DeterministicReport{Forecast: forecast, HistoricalWins: wins, Opportunities: opportunities, Risks: risks, FallbackBriefs: BuildFallback(bundle)} if input.Peer != nil { peer := EvaluatePeer(*input.Peer) report.Peer = &peer } return report } func TehranDay(value time.Time) string { return value.In(tehranLocation()).Format(time.DateOnly) } func NextRefreshAt(lastRefresh time.Time) time.Time { local := lastRefresh.In(tehranLocation()) nextLocal := time.Date(local.Year(), local.Month(), local.Day()+1, 0, 0, 0, 0, tehranLocation()) return nextLocal.UTC() } func CanManualRefresh(lastRefresh, now time.Time) bool { if lastRefresh.IsZero() { return true } return !now.Before(NextRefreshAt(lastRefresh)) } func BuildFallback(bundle EvidenceBundle) []CampaignBrief { briefs := make([]CampaignBrief, 0, len(bundle.Opportunities)+len(bundle.Risks)) for _, opportunity := range bundle.Opportunities { briefs = append(briefs, CampaignBrief{ HeadlineFA: "یک فرصت مناسب در راه است: " + opportunity.EventTitleFA, GuidanceFA: "داده‌های قبلی شما نشان می‌دهد این مناسبت می‌تواند برای برنامه‌ریزی فروش ارزشمند باشد.", OfferFA: "پیشنهاد خود را متناسب با نیاز مشتریان این مناسبت آماده کنید.", PreparationFA: "موجودی، پیام کمپین و مسیر پرداخت را پیش از شروع مناسبت بررسی کنید.", LimitationFA: "این پیشنهاد بر پایه الگوهای گذشته است و نتیجه قطعی نیست.", EvidenceIDs: []string{opportunity.EvidenceID}, }) } for _, risk := range bundle.Risks { briefs = append(briefs, CampaignBrief{ HeadlineFA: "برای یک بازه آرام‌تر آماده باشید", GuidanceFA: "الگوی گذشته احتمال کاهش فعالیت را در این بازه نشان می‌دهد.", OfferFA: "یک پیشنهاد سبک برای حفظ ارتباط با مشتریان در نظر بگیرید.", PreparationFA: "پیام یادآوری و تجربه پرداخت را پیش از این بازه بازبینی کنید.", LimitationFA: "این هشدار بر پایه الگوهای گذشته است و نتیجه قطعی نیست.", EvidenceIDs: []string{risk.EvidenceID}, }) } return briefs } func EvidenceID(kind, stableKey string) string { digest := sha256.Sum256([]byte(kind + "\x00" + stableKey)) return fmt.Sprintf("%s_%s", kind, hex.EncodeToString(digest[:8])) } func tehranLocation() *time.Location { location, err := time.LoadLocation("Asia/Tehran") if err != nil { return time.FixedZone("Asia/Tehran", 3*60*60+30*60) } return location }