package growth import ( "fmt" "slices" "strings" "time" ) const officialCalendarSource = "https://calendar.ut.ac.ir/" const curatedCommercialSource = "CURATED_COMMERCIAL_1405_V1" var importedMerchantCategories = []string{ "ارائه دهنده خدمات اینترنت", "مراکز آموزشی مجازی", "کیف و کفش فروشی", "خدمات شبکه‌های کامپیوتری و اینترنت", "فروشگاه لوازم آرایشی و بهداشتی", } // Catalog1405 returns a defensive copy of the reviewed initial catalog. Dates are // Gregorian Tehran-local calendar dates and cover the imported 1405 history plus // the first 90 forecast days after 2026-06-30. func Catalog1405() []Event { events := []Event{ catalogEvent("nowruz-1405", "نوروز", "2026-03-21", "2026-03-24", "new-year", EventOfficial, []string{"هدیه", "خرید", "سال-نو"}), catalogEvent("nature-day-1405", "روز طبیعت", "2026-04-02", "2026-04-02", "leisure", EventOfficial, []string{"تفریح", "خرید"}), catalogEvent("teachers-day-1405", "روز معلم", "2026-05-02", "2026-05-02", "appreciation", EventOfficial, []string{"هدیه", "آموزش", "قدردانی"}), catalogEvent("khordad-15-1405", "قیام پانزده خرداد", "2026-06-05", "2026-06-05", "official-holiday", EventOfficial, []string{"تعطیل"}), catalogEvent("doctors-day-1405", "روز پزشک", "2026-08-23", "2026-08-23", "appreciation", EventOfficial, []string{"هدیه", "قدردانی", "حرفه‌ای"}), catalogEvent("back-to-school-1405", "آمادگی بازگشایی مدارس", "2026-09-10", "2026-09-22", "education-season", EventCommercial, []string{"آموزش", "خرید", "مدرسه"}), } result := make([]Event, len(events)) for index, event := range events { result[index] = event result[index].Tags = slices.Clone(event.Tags) result[index].Categories = slices.Clone(event.Categories) } return result } func catalogEvent(id, title, start, end, family string, kind EventKind, tags []string) Event { source := officialCalendarSource if kind == EventCommercial { source = curatedCommercialSource } return Event{ID: id, TitleFA: title, StartDate: mustCatalogDate(start), EndDate: mustCatalogDate(end), Family: family, Tags: slices.Clone(tags), Source: source, Categories: slices.Clone(importedMerchantCategories), Kind: kind} } func ValidateCatalog(events []Event) error { seen := make(map[string]struct{}, len(events)) for index, event := range events { if strings.TrimSpace(event.ID) == "" || strings.TrimSpace(event.TitleFA) == "" || strings.TrimSpace(event.Family) == "" || strings.TrimSpace(event.Source) == "" { return fmt.Errorf("growth catalog event %d is incomplete", index) } if _, exists := seen[event.ID]; exists { return fmt.Errorf("growth catalog contains duplicate id %q", event.ID) } seen[event.ID] = struct{}{} if event.StartDate.IsZero() || event.EndDate.Before(event.StartDate) { return fmt.Errorf("growth catalog event %q has invalid date range", event.ID) } if event.Kind != EventOfficial && event.Kind != EventCommercial { return fmt.Errorf("growth catalog event %q has invalid kind", event.ID) } if len(normalizedSet(event.Tags)) == 0 || len(event.Categories) == 0 { return fmt.Errorf("growth catalog event %q lacks tags or supported categories", event.ID) } } return nil } func mustCatalogDate(value string) time.Time { parsed, err := time.Parse(time.DateOnly, value) if err != nil { panic("invalid reviewed event catalog date: " + value) } return parsed }