Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-05 23:43:17 +03:00
parent 9e1ecb9b3f
commit 53cdd1a4a1
4 changed files with 421 additions and 142 deletions
+219 -52
View File
@@ -445,12 +445,12 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
`
UPDATE mk_product_performance_price_dim
SET
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
base_price_usd = LEAST(cost_price_usd, base_price_usd),
cost_price_usd = LEAST(cost_price_usd, base_price_usd),
base_price_usd = GREATEST(cost_price_usd, base_price_usd),
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
AND cost_price_usd > base_price_usd`,
`
DELETE FROM mk_product_performance_price_dim
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
@@ -527,20 +527,20 @@ WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00D
`
UPDATE mk_product_performance_kpi_daily
SET
cost_price_usd = GREATEST(cost_price_usd, base_price_usd),
base_price_usd = LEAST(cost_price_usd, base_price_usd),
gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd)),
gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd)),
gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * GREATEST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
cost_price_usd = LEAST(cost_price_usd, base_price_usd),
base_price_usd = GREATEST(cost_price_usd, base_price_usd),
gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd)),
gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd)),
gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
AND cost_price_usd > base_price_usd`,
`
UPDATE mk_product_performance_kpi_daily
SET askili_yan = '', updated_at = now()
@@ -4486,7 +4486,29 @@ FROM (
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(performance_score * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS performance_score,
CASE
WHEN SUM(CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END) > 0
THEN SUM(performance_score * CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END) / NULLIF(SUM(CASE
WHEN COALESCE(sales_usd_90d,0) > 0 THEN sales_usd_90d
WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
WHEN COALESCE(stock_qty,0) > 0 AND COALESCE(cost_price_usd,0) > 0 THEN stock_qty * cost_price_usd
WHEN COALESCE(stock_qty,0) > 0 THEN stock_qty
ELSE 1
END),0)
ELSE 0
END AS performance_score,
MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket,
COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(recommendation,'')), '') AS recommendation,
COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN idle_cost_usd ELSE 0 END),0) AS idle_cost_usd
@@ -5167,7 +5189,7 @@ type productPerformanceGroupedSnapshotNode struct {
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockSeen map[string]bool
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
@@ -5235,19 +5257,8 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
continue
}
switch {
case key == "stock_qty":
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
continue
}
if n.StockSeen == nil {
n.StockSeen = map[string]bool{}
}
if !n.StockSeen[variantKey] {
n.StockSeen[variantKey] = true
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
}
case isProductPerformanceDistinctVariantStockMetric(key):
n.addDistinctVariantMetric(row, key, value)
case key == "idle_cost_usd":
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
@@ -5289,6 +5300,27 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
n.addDistinctSpread(row)
}
func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map[string]any, key string, value any) {
variantKey := productPerformanceMapVariantKey(row)
if variantKey == "" {
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
return
}
if n.StockMetricSeen == nil {
n.StockMetricSeen = map[string]map[string]bool{}
}
seen := n.StockMetricSeen[key]
if seen == nil {
seen = map[string]bool{}
n.StockMetricSeen[key] = seen
}
if seen[variantKey] {
return
}
seen[variantKey] = true
n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
}
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
@@ -5355,11 +5387,11 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
row[key] = state.Sum / state.Count
}
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
deriveProductPerformanceGroupMetrics(row, n.Field)
if bucket := n.dominantBucket(); bucket != "" {
row["performance_bucket"] = bucket
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
clearProductPerformanceGroupDimensions(row, n.Field, n.Value)
row["__group"] = true
row["row_key"] = "group|" + n.Key
@@ -5489,8 +5521,8 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
if isProductPerformanceMarginField(key) {
continue
}
if key == "stock_qty" {
out[key] = distinctProductPerformanceVariantStockQty(rows)
if isProductPerformanceDistinctVariantStockMetric(key) {
out[key] = distinctProductPerformanceVariantNumber(rows, key)
continue
}
if key == "idle_cost_usd" {
@@ -5517,9 +5549,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
}
}
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
deriveProductPerformanceGroupMetrics(out, groupField)
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
return out
}
@@ -5613,7 +5645,11 @@ func isProductPerformanceMarginField(field string) bool {
return strings.HasPrefix(field, "gross_margin")
}
func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
func isProductPerformanceDistinctVariantStockMetric(field string) bool {
return field == "stock_qty" || strings.HasPrefix(field, "avg_stock_")
}
func distinctProductPerformanceVariantNumber(rows []map[string]any, field string) float64 {
seen := map[string]bool{}
total := 0.0
hasKey := false
@@ -5627,13 +5663,13 @@ func distinctProductPerformanceVariantStockQty(rows []map[string]any) float64 {
continue
}
seen[key] = true
total += floatFromMap(row, "stock_qty")
total += floatFromMap(row, field)
}
if hasKey {
return total
}
for _, row := range rows {
total += floatFromMap(row, "stock_qty")
total += floatFromMap(row, field)
}
return total
}
@@ -5674,36 +5710,57 @@ func productPerformanceMapVariantKey(row map[string]any) string {
}
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
normalizeProductPerformanceCostFields(out)
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
sales := floatFromMap(out, "sales_usd_"+suffix)
qty := floatFromMap(out, "sales_qty_"+suffix)
stockQty := floatFromMap(out, "stock_qty")
if _, exists := out["stock_turnover_"+suffix]; !exists && stockQty > 0 {
out["stock_turnover_"+suffix] = qty / stockQty
turnoverBase := floatFromMap(out, "avg_stock_"+suffix)
if turnoverBase <= 0 {
turnoverBase = stockQty
}
if turnoverBase > 0 {
out["stock_turnover_"+suffix] = qty / turnoverBase
} else {
out["stock_turnover_"+suffix] = 0
}
if qty > 0 {
out["avg_price_usd_"+suffix] = sales / qty
}
basePrice, hasBasePrice := productPerformanceUnitCostForSuffix(out, "base_price_usd", suffix)
costPrice, hasCostPrice := productPerformanceUnitCostForSuffix(out, "cost_price_usd", suffix)
if hasBasePrice {
out["base_price_usd_"+suffix] = basePrice
}
if hasCostPrice {
out["cost_price_usd_"+suffix] = costPrice
}
if qty > 0 && hasBasePrice {
out["unit_profit_base_"+suffix] = (sales / qty) - basePrice
avgPrice := sales / qty
out["unit_profit_base_"+suffix] = avgPrice - basePrice
out["gross_profit_base_usd_"+suffix] = sales - (qty * basePrice)
} else {
out["unit_profit_base_"+suffix] = 0
out["gross_profit_base_usd_"+suffix] = 0
}
if qty > 0 && hasCostPrice {
out["unit_profit_cost_"+suffix] = (sales / qty) - costPrice
avgPrice := sales / qty
out["unit_profit_cost_"+suffix] = avgPrice - costPrice
out["gross_profit_cost_usd_"+suffix] = sales - (qty * costPrice)
out["gross_profit_usd_"+suffix] = out["gross_profit_cost_usd_"+suffix]
} else {
out["unit_profit_cost_"+suffix] = 0
out["gross_profit_cost_usd_"+suffix] = 0
out["gross_profit_usd_"+suffix] = 0
}
if sales > 0 {
if _, ok := out["gross_profit_base_usd_"+suffix]; ok {
out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
}
if _, ok := out["gross_profit_cost_usd_"+suffix]; ok {
out["gross_margin_cost_"+suffix] = floatFromMap(out, "gross_profit_cost_usd_"+suffix) / sales
}
if _, ok := out["gross_profit_usd_"+suffix]; ok {
out["gross_margin_"+suffix] = floatFromMap(out, "gross_profit_usd_"+suffix) / sales
}
out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
out["gross_margin_cost_"+suffix] = floatFromMap(out, "gross_profit_cost_usd_"+suffix) / sales
out["gross_margin_"+suffix] = floatFromMap(out, "gross_profit_usd_"+suffix) / sales
} else {
out["gross_margin_base_"+suffix] = 0
out["gross_margin_cost_"+suffix] = 0
out["gross_margin_"+suffix] = 0
}
}
orderUSD := floatFromMap(out, "order_usd")
@@ -5743,7 +5800,11 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
}
if _, ok := out["performance_score"]; !ok {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
if score, exists := productPerformanceOptionalFloat(out, "performance_score_90d"); exists {
out["performance_score"] = score
} else {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
}
}
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
score := productPerformanceOrderGroupScore(out)
@@ -5751,16 +5812,53 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
out["performance_score_180d"] = score
out["performance_score_365d"] = score
out["performance_score_total"] = score
out["performance_score"] = score
}
}
func normalizeProductPerformanceCostFields(row map[string]any) {
normalize := func(costField, baseField string) {
cost, hasCost := productPerformanceOptionalFloat(row, costField)
base, hasBase := productPerformanceOptionalFloat(row, baseField)
if !hasCost && !hasBase {
return
}
cost, base = normalizeProductPerformanceCostPair(cost, base)
if hasCost || cost > 0 {
row[costField] = cost
}
if hasBase || base > 0 {
row[baseField] = base
}
}
normalize("cost_price_usd", "base_price_usd")
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
}
}
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
value, ok := row[field]
if !ok {
return 0, false
}
return floatFromAny(value), true
}
func productPerformanceUnitCostForSuffix(row map[string]any, baseField, suffix string) (float64, bool) {
hasSuffix := false
if value, ok := row[baseField+"_"+suffix]; ok {
return floatFromAny(value), true
hasSuffix = true
if n := floatFromAny(value); n > 0 {
return n, true
}
}
if value, ok := row[baseField]; ok {
return floatFromAny(value), true
}
if hasSuffix {
return 0, true
}
return 0, false
}
@@ -6099,6 +6197,7 @@ func shouldAverageProductPerformanceField(field string) bool {
strings.HasPrefix(field, "expected_margin") ||
strings.HasPrefix(field, "sales_index") ||
strings.HasPrefix(field, "performance_score") ||
strings.HasPrefix(field, "customer_score") ||
strings.HasPrefix(field, "stock_days") ||
strings.HasPrefix(field, "stock_turnover")
}
@@ -6136,6 +6235,9 @@ func productPerformanceUsesCostWeight(field string) bool {
}
func productPerformanceMetricWeight(row map[string]any, field string) float64 {
if productPerformanceUsesScoreWeight(field) {
return productPerformanceScoreWeight(row, productPerformanceScoreSuffix(field))
}
weightField := productPerformanceMetricWeightField(field)
if weightField == "__product_cost_weight" {
return productPerformanceCostWeight(row)
@@ -6143,6 +6245,44 @@ func productPerformanceMetricWeight(row map[string]any, field string) float64 {
return floatFromMap(row, weightField)
}
func productPerformanceUsesScoreWeight(field string) bool {
return strings.HasPrefix(field, "performance_score") ||
strings.HasPrefix(field, "customer_score")
}
func productPerformanceScoreSuffix(field string) string {
switch {
case strings.Contains(field, "_180d"):
return "180d"
case strings.Contains(field, "_365d"):
return "365d"
case strings.Contains(field, "_total"):
return "total"
default:
return "90d"
}
}
func productPerformanceScoreWeight(row map[string]any, suffix string) float64 {
if weight := floatFromMap(row, "sales_usd_"+suffix); weight > 0 {
return weight
}
if weight := floatFromMap(row, "sales_qty_"+suffix); weight > 0 {
return weight
}
stockQty := floatFromMap(row, "stock_qty")
if stockQty <= 0 {
return 1
}
if cost := floatFromMap(row, "cost_price_usd_"+suffix); cost > 0 {
return stockQty * cost
}
if cost := floatFromMap(row, "cost_price_usd"); cost > 0 {
return stockQty * cost
}
return stockQty
}
func productPerformanceCostWeight(row map[string]any) float64 {
if weight := floatFromMap(row, "sales_qty_total"); weight > 0 {
return weight
@@ -6156,6 +6296,9 @@ func productPerformanceCostWeight(row map[string]any) float64 {
}
func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 {
if productPerformanceUsesScoreWeight(valueField) {
return weightedAverageProductPerformanceScoreRows(rows, valueField, productPerformanceScoreSuffix(valueField))
}
var weighted, qty float64
for _, row := range rows {
value := floatFromMap(row, valueField)
@@ -6177,6 +6320,30 @@ func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qt
return 0
}
func weightedAverageProductPerformanceScoreRows(rows []map[string]any, valueField, suffix string) float64 {
var weighted, weight, sum, count float64
for _, row := range rows {
value, ok := productPerformanceOptionalFloat(row, valueField)
if !ok {
continue
}
rowWeight := productPerformanceScoreWeight(row, suffix)
if rowWeight > 0 {
weighted += value * rowWeight
weight += rowWeight
}
sum += value
count++
}
if weight > 0 {
return weighted / weight
}
if count > 0 {
return sum / count
}
return 0
}
func averageProductPerformanceDistinctVariantField(rows []map[string]any, valueField string) float64 {
seen := map[string]bool{}
sum := 0.0
@@ -6642,7 +6809,7 @@ func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) strin
}
func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD < basePriceUSD {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD