fix: deliver validated AI insight responses
This commit is contained in:
@@ -40,8 +40,13 @@ const (
|
||||
|
||||
type Event struct {
|
||||
ID string `json:"id"`
|
||||
OccasionKey string `json:"occasionKey,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
TitleFA string `json:"titleFa"`
|
||||
TitleEN string `json:"titleEn,omitempty"`
|
||||
Aliases []string `json:"aliases,omitempty"`
|
||||
Themes []string `json:"themes,omitempty"`
|
||||
CampaignTags []string `json:"campaignTags,omitempty"`
|
||||
StartDate string `json:"startDate"`
|
||||
EndDate string `json:"endDate"`
|
||||
Recurrence Recurrence `json:"recurrence,omitempty"`
|
||||
@@ -60,6 +65,7 @@ type Event struct {
|
||||
LagDays int `json:"lagDays"`
|
||||
RelevanceBPS int `json:"relevanceBps"`
|
||||
ConfidenceBPS int `json:"confidenceBps"`
|
||||
ImportanceBPS int `json:"importanceBps,omitempty"`
|
||||
}
|
||||
|
||||
func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time.Time, error) {
|
||||
@@ -79,6 +85,7 @@ func ResolveRecurrence(rule Recurrence, year int, location *time.Location) (time
|
||||
|
||||
func ValidateEvents(events []Event) error {
|
||||
seen := make(map[string]struct{}, len(events))
|
||||
canonical := make(map[string]struct{}, len(events))
|
||||
for _, event := range events {
|
||||
if strings.TrimSpace(event.ID) == "" || strings.TrimSpace(event.TitleFA) == "" || strings.TrimSpace(event.SourceURI) == "" || strings.TrimSpace(event.SourceVersion) == "" || strings.TrimSpace(event.InclusionRationale) == "" || strings.TrimSpace(event.GeographicScope) == "" {
|
||||
return fmt.Errorf("event catalog: event %q is incomplete", event.ID)
|
||||
@@ -87,6 +94,15 @@ func ValidateEvents(events []Event) error {
|
||||
return fmt.Errorf("event catalog: duplicate event %q", event.ID)
|
||||
}
|
||||
seen[event.ID] = struct{}{}
|
||||
if event.OccasionKey == "" {
|
||||
event.OccasionKey = event.ID
|
||||
}
|
||||
if event.Variant == "" {
|
||||
event.Variant = "PRIMARY"
|
||||
}
|
||||
if event.Variant != "PRIMARY" && event.Variant != "CAMPAIGN_WINDOW" && event.Variant != "OPERATIONAL_WINDOW" {
|
||||
return fmt.Errorf("event catalog: event %q has invalid variant", event.ID)
|
||||
}
|
||||
start, err := time.Parse(time.DateOnly, event.StartDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("event catalog: event %q start date: %w", event.ID, err)
|
||||
@@ -101,7 +117,7 @@ func ValidateEvents(events []Event) error {
|
||||
if (event.Tone == ToneMourning || event.Tone == ToneMemorial) && event.CommercialPosture == PosturePromote {
|
||||
return fmt.Errorf("event catalog: event %q cannot promote during mourning or memorial", event.ID)
|
||||
}
|
||||
if event.LeadDays < 0 || event.LagDays < 0 || event.RelevanceBPS < 0 || event.RelevanceBPS > 10_000 || event.ConfidenceBPS < 0 || event.ConfidenceBPS > 10_000 {
|
||||
if event.LeadDays < 0 || event.LagDays < 0 || event.RelevanceBPS < 0 || event.RelevanceBPS > 10_000 || event.ConfidenceBPS < 0 || event.ConfidenceBPS > 10_000 || event.ImportanceBPS < 0 || event.ImportanceBPS > 10_000 {
|
||||
return fmt.Errorf("event catalog: event %q has invalid impact metadata", event.ID)
|
||||
}
|
||||
if event.LeadDays > 90 || event.LagDays > 90 {
|
||||
@@ -116,6 +132,11 @@ func ValidateEvents(events []Event) error {
|
||||
return fmt.Errorf("event catalog: event %q does not match its resolved recurrence", event.ID)
|
||||
}
|
||||
}
|
||||
canonicalKey := strings.Join([]string{event.OccasionKey, event.Variant, event.StartDate, event.EndDate}, "\x00")
|
||||
if _, exists := canonical[canonicalKey]; exists {
|
||||
return fmt.Errorf("event catalog: duplicate canonical occurrence %q", event.ID)
|
||||
}
|
||||
canonical[canonicalKey] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
44
internal/eventcatalog/impactful_catalog_test.go
Normal file
44
internal/eventcatalog/impactful_catalog_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package eventcatalog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestImpactfulBusinessCatalogIsValidAndIndustryPersonalized(t *testing.T) {
|
||||
data, err := os.ReadFile("../../catalogs/impactful-business-events-1405-v1.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var input Import
|
||||
if err := json.Unmarshal(data, &input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ValidateImport(input, 1405); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hasEducation, hasRetail := false, false
|
||||
months := make(map[time.Month]bool)
|
||||
for _, event := range input.Events {
|
||||
if event.OfficialHoliday && event.RelevanceBPS < 7000 {
|
||||
t.Fatalf("official holiday %s lacks evidenced business impact", event.ID)
|
||||
}
|
||||
for _, category := range event.MerchantCategories {
|
||||
hasEducation = hasEducation || category == "مراکز آموزشی مجازی"
|
||||
hasRetail = hasRetail || category == "کیف و کفش فروشی"
|
||||
}
|
||||
start, err := time.Parse(time.DateOnly, event.StartDate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
months[start.Month()] = true
|
||||
}
|
||||
if !hasEducation || !hasRetail {
|
||||
t.Fatal("catalog must include both education- and retail-specific windows")
|
||||
}
|
||||
if len(input.Events) < 12 || len(months) < 9 {
|
||||
t.Fatalf("catalog must provide year-round candidate coverage; got %d events across %d months", len(input.Events), len(months))
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,12 @@ func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
|
||||
return fmt.Errorf("creating calendar catalog version: %w", err)
|
||||
}
|
||||
for _, event := range input.Events {
|
||||
if event.OccasionKey == "" {
|
||||
event.OccasionKey = event.ID
|
||||
}
|
||||
if event.Variant == "" {
|
||||
event.Variant = "PRIMARY"
|
||||
}
|
||||
categories, err := json.Marshal(event.MerchantCategories)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding calendar event categories: %w", err)
|
||||
@@ -70,8 +76,20 @@ func (r *Repository) ImportDraft(ctx context.Context, input Import) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding calendar event channels: %w", err)
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO calendar_events(catalog_version_id,event_key,title_fa,title_en,start_date,end_date,recurrence_rule,event_type,tone,commercial_posture,official_holiday,source_uri,source_version,source_checksum,inclusion_rationale,geographic_scope,merchant_categories,behavioral_channels,lead_days,lag_days,relevance_bps,confidence_bps)
|
||||
VALUES($1,$2,$3,nullif($4,''),$5,$6,nullif($7,''),$8,$9,$10,$11,$12,$13,nullif($14,''),$15,$16,$17,$18,$19,$20,$21,$22)`, versionID, event.ID, event.TitleFA, event.TitleEN, event.StartDate, event.EndDate, event.Recurrence, event.Type, event.Tone, event.CommercialPosture, event.OfficialHoliday, event.SourceURI, event.SourceVersion, event.SourceChecksum, event.InclusionRationale, event.GeographicScope, categories, channels, event.LeadDays, event.LagDays, event.RelevanceBPS, event.ConfidenceBPS)
|
||||
aliases, err := json.Marshal(event.Aliases)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding calendar event aliases: %w", err)
|
||||
}
|
||||
themes, err := json.Marshal(event.Themes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding calendar event themes: %w", err)
|
||||
}
|
||||
tags, err := json.Marshal(event.CampaignTags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding calendar event campaign tags: %w", err)
|
||||
}
|
||||
_, err = tx.Exec(ctx, `INSERT INTO calendar_events(catalog_version_id,event_key,occasion_key,event_variant,title_fa,title_en,aliases,themes,campaign_tags,start_date,end_date,recurrence_rule,event_type,tone,commercial_posture,official_holiday,source_uri,source_version,source_checksum,inclusion_rationale,geographic_scope,merchant_categories,behavioral_channels,lead_days,lag_days,relevance_bps,confidence_bps,importance_bps)
|
||||
VALUES($1,$2,$3,$4,$5,nullif($6,''),$7,$8,$9,$10,$11,nullif($12,''),$13,$14,$15,$16,$17,$18,nullif($19,''),$20,$21,$22,$23,$24,$25,$26,$27,$28)`, versionID, event.ID, event.OccasionKey, event.Variant, event.TitleFA, event.TitleEN, aliases, themes, tags, event.StartDate, event.EndDate, event.Recurrence, event.Type, event.Tone, event.CommercialPosture, event.OfficialHoliday, event.SourceURI, event.SourceVersion, event.SourceChecksum, event.InclusionRationale, event.GeographicScope, categories, channels, event.LeadDays, event.LagDays, event.RelevanceBPS, event.ConfidenceBPS, event.ImportanceBPS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting calendar event %q: %w", event.ID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user