diff --git a/svc/queries/product_performance.go b/svc/queries/product_performance.go index f156b51..7ffe5b2 100644 --- a/svc/queries/product_performance.go +++ b/svc/queries/product_performance.go @@ -5229,15 +5229,30 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql return nil, nil } if strings.TrimSpace(mode) == "idle" { + normalizeProductPerformanceGroupedSourceScores(rows, mode) return productPerformanceIdleSourceRows(rows), nil } if mode == "products" || mode == "product_detail" { rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows) rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows) } + normalizeProductPerformanceGroupedSourceScores(rows, mode) return rows, nil } +func normalizeProductPerformanceGroupedSourceScores(rows []map[string]any, mode string) { + if !shouldCascadeProductPerformanceGroupedScores(mode) { + return + } + for _, row := range rows { + deriveProductPerformanceGroupMetrics(row, "") + for _, suffix := range productPerformancePeriodSuffixes() { + row["performance_score_"+suffix] = productPerformanceSalesPeriodScore(row, suffix) + } + row["performance_score"] = row["performance_score_90d"] + } +} + func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any { if len(rows) == 0 { return rows @@ -5518,9 +5533,82 @@ func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, lev out := make([]map[string]any, 0, len(sourceRows)) appendProductPerformanceGroupedSnapshotNodes(&out, roots, rootOrder) + applyProductPerformanceGroupedChildScoreAverages(out, mode) return out } +func applyProductPerformanceGroupedChildScoreAverages(rows []map[string]any, mode string) { + if !shouldCascadeProductPerformanceGroupedScores(mode) || len(rows) == 0 { + return + } + byKey := make(map[string]map[string]any, len(rows)) + children := map[string][]map[string]any{} + maxLevel := -1 + for _, row := range rows { + key := stringFromMap(row, "key") + if key == "" { + continue + } + byKey[key] = row + if level := intFromMap(row, "level"); level > maxLevel { + maxLevel = level + } + } + for _, row := range rows { + key := stringFromMap(row, "key") + parentKey := productPerformanceParentGroupKey(key) + if parentKey == "" || byKey[parentKey] == nil { + continue + } + children[parentKey] = append(children[parentKey], row) + } + for level := maxLevel - 1; level >= 0; level-- { + for _, row := range rows { + if intFromMap(row, "level") != level { + continue + } + childRows := children[stringFromMap(row, "key")] + if len(childRows) == 0 { + continue + } + for _, suffix := range productPerformancePeriodSuffixes() { + field := "performance_score_" + suffix + if !productPerformanceRowsHaveField(childRows, field) { + continue + } + row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix) + } + if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok { + row["performance_score"] = score + } + } + } +} + +func shouldCascadeProductPerformanceGroupedScores(mode string) bool { + switch strings.TrimSpace(mode) { + case "products", + "product_detail", + "idle", + "sales_color_yaka_market_customer", + "sales_product_country_segment_market_customer", + "sales_market_customer_product", + "sales_country_segment_market_customer_product": + return true + default: + return false + } +} + +func productPerformanceRowsHaveField(rows []map[string]any, field string) bool { + for _, row := range rows { + if _, ok := productPerformanceOptionalFloat(row, field); ok { + return true + } + } + return false +} + func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) { n.Count++ if n.Image == nil && stringFromMap(row, "product_code") != "" {