Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-07 12:59:19 +03:00
parent 005d2eafac
commit c6a3d1552e
4 changed files with 256 additions and 66 deletions
+119 -27
View File
@@ -1901,6 +1901,7 @@ func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow
rows[i].StockTurnoverTotal,
float64(rows[i].MarketCountTotal),
float64(rows[i].CustomerCountTotal),
productPerformanceProductRowTotalPeriodDays(rows[i]),
)
rows[i].PerformanceScore = rows[i].PerformanceScore90
}
@@ -1949,6 +1950,10 @@ func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) p
return out
}
func productPerformanceProductRowTotalPeriodDays(row models.ProductPerformanceRow) float64 {
return productPerformancePeriodDays(map[string]any{"kpi_date": row.KpiDate}, "total")
}
func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 {
if salesUSD <= 0 {
return 0
@@ -2286,7 +2291,7 @@ SELECT
WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'YILDIZ_URUN'
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'YILDIZ_URUN'
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
ELSE 'TAKIP'
END AS performance_bucket,
@@ -2294,7 +2299,7 @@ SELECT
WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.25 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
ELSE 'Izleme ve piyasa bazli aksiyon.'
END AS recommendation,
@@ -3581,6 +3586,19 @@ StockTotalStart AS (
WHERE stock_date <= DATE '2022-01-01'
ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
FirstSaleByProduct AS (
SELECT
product_code,
color_code,
yaka_kodu,
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days') AS first_sale_date_90d,
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days') AS first_sale_date_180d,
MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days') AS first_sale_date_365d,
MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total
FROM mk_product_performance_sales_daily
WHERE sales_date >= DATE '2022-01-01'
GROUP BY product_code, color_code, yaka_kodu
),
Agg AS (
SELECT
$2::text AS breakdown,
@@ -3683,18 +3701,55 @@ Agg AS (
COALESCE(pd.base_price_usd,0) AS base_price_usd,
COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
COALESCE(st.stock_qty,0) AS stock_qty,
(COALESCE(s90.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
(COALESCE(s180.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
(COALESCE(s365.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
(COALESCE(stotal.stock_qty,0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
(COALESCE(s90.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
(COALESCE(s180.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
(COALESCE(s365.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
(COALESCE(stotal.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
FROM mk_product_performance_sales_daily s
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
LEFT JOIN FirstSaleByProduct fs ON fs.product_code = s.product_code AND fs.color_code = s.color_code AND fs.yaka_kodu = s.yaka_kodu
LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
LEFT JOIN Stock90Start s90 ON s90.product_code = s.product_code AND s90.color_code = s.color_code AND s90.yaka_kodu = s.yaka_kodu
LEFT JOIN Stock180Start s180 ON s180.product_code = s.product_code AND s180.color_code = s.color_code AND s180.yaka_kodu = s.yaka_kodu
LEFT JOIN Stock365Start s365 ON s365.product_code = s.product_code AND s365.color_code = s.color_code AND s365.yaka_kodu = s.yaka_kodu
LEFT JOIN StockTotalStart stotal ON stotal.product_code = s.product_code AND stotal.color_code = s.color_code AND stotal.yaka_kodu = s.yaka_kodu
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code = s.product_code
AND x.color_code = s.color_code
AND x.yaka_kodu = s.yaka_kodu
AND x.stock_date <= COALESCE(fs.first_sale_date_90d, current_date - INTERVAL '89 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s90 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code = s.product_code
AND x.color_code = s.color_code
AND x.yaka_kodu = s.yaka_kodu
AND x.stock_date <= COALESCE(fs.first_sale_date_180d, current_date - INTERVAL '179 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s180 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code = s.product_code
AND x.color_code = s.color_code
AND x.yaka_kodu = s.yaka_kodu
AND x.stock_date <= COALESCE(fs.first_sale_date_365d, current_date - INTERVAL '359 days')
ORDER BY x.stock_date DESC
LIMIT 1
) s365 ON TRUE
LEFT JOIN LATERAL (
SELECT x.stock_qty
FROM mk_product_performance_stock_daily x
WHERE x.product_code = s.product_code
AND x.color_code = s.color_code
AND x.yaka_kodu = s.yaka_kodu
AND x.stock_date <= COALESCE(fs.first_sale_date_total, DATE '2022-01-01')
ORDER BY x.stock_date DESC
LIMIT 1
) stotal ON TRUE
) s
WHERE sales_date >= DATE '2022-01-01'
AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
@@ -3813,7 +3868,7 @@ SELECT
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
ROUND((
LEAST(35, sales_usd_90d / 25000 * 35)
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
+ LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
+ LEAST(20, product_group_count_90d / 8.0 * 20)
+ LEAST(15, sales_qty_90d / 500 * 15)
)::numeric, 4)
@@ -3821,7 +3876,7 @@ SELECT
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE
ROUND((
LEAST(35, sales_usd_180d / 50000 * 35)
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.40 * 30)
+ LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.45 * 30)
+ LEAST(20, product_group_count_180d / 8.0 * 20)
+ LEAST(15, sales_qty_180d / 1000 * 15)
)::numeric, 4)
@@ -3829,7 +3884,7 @@ SELECT
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE
ROUND((
LEAST(35, sales_usd_365d / 100000 * 35)
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.40 * 30)
+ LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.45 * 30)
+ LEAST(20, product_group_count_365d / 8.0 * 20)
+ LEAST(15, sales_qty_365d / 2000 * 15)
)::numeric, 4)
@@ -3837,18 +3892,18 @@ SELECT
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE
ROUND((
LEAST(35, sales_usd_total / 300000 * 35)
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.40 * 30)
+ LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.45 * 30)
+ LEAST(20, product_group_count_total / 8.0 * 20)
+ LEAST(15, sales_qty_total / 6000 * 15)
)::numeric, 4)
END AS customer_score_total,
CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
ROUND(
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.40 * 30)
LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
+ LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END) / 4.0 * 20)
+ LEAST(20, sales_usd_90d / 10000 * 20)
+ LEAST(15, market_count_90d / 8.0 * 15)
+ LEAST(15, customer_count_90d / 25.0 * 15),
+ LEAST(20, sales_usd_90d / 25000 * 20)
+ LEAST(5, market_count_90d / 3.0 * 5)
+ LEAST(25, customer_count_90d / 8.0 * 25),
4
)
END AS performance_score,
@@ -6249,6 +6304,7 @@ func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float
stockTurnover,
productPerformancePeriodCount(row, "market_count", suffix),
productPerformancePeriodCount(row, "customer_count", suffix),
productPerformancePeriodDays(row, suffix),
)
}
@@ -6292,16 +6348,17 @@ func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
}
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64) float64 {
func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64, periodDays ...float64) float64 {
if salesUSD <= 0 {
return 1
}
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTarget(suffix))
days := productPerformanceScorePeriodDays(suffix, periodDays...)
revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTargetForDays(suffix, days))
score := 0.30*productPerformanceMarginComponentScore(margin) +
0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) +
0.20*revenueScore +
0.05*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) +
0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTarget(suffix))
0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTargetForDays(suffix, days))
return productPerformanceRoundScore(score)
}
@@ -6323,6 +6380,10 @@ func productPerformanceMarketSpreadTarget(suffix string) float64 {
}
func productPerformanceCustomerSpreadTarget(suffix string) float64 {
return productPerformanceCustomerSpreadTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
}
func productPerformanceCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "90d":
return 8
@@ -6331,7 +6392,7 @@ func productPerformanceCustomerSpreadTarget(suffix string) float64 {
case "365d":
return 50
default:
return 50
return 20 * productPerformanceTotalPeriodMultiplier(periodDays)
}
}
@@ -6358,7 +6419,7 @@ func productPerformanceRelativeIndex(value, average float64) float64 {
}
func productPerformanceMarginComponentScore(margin float64) float64 {
return productPerformanceRatioScore(maxFloat(0, margin), 0.40)
return productPerformanceRatioScore(maxFloat(0, margin), 0.45)
}
func productPerformanceRatioScore(value, target float64) float64 {
@@ -6379,18 +6440,49 @@ func productPerformanceRoundScore(score float64) float64 {
}
func productPerformanceProductRevenueTarget(suffix string) float64 {
return productPerformanceProductRevenueTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
}
func productPerformanceProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
switch suffix {
case "180d":
return 20000
return 50000
case "365d":
return 40000
return 100000
case "total":
return 120000
return 50000 * productPerformanceTotalPeriodMultiplier(periodDays)
default:
return 10000
return 25000
}
}
func productPerformanceScorePeriodDays(suffix string, periodDays ...float64) float64 {
if len(periodDays) > 0 && periodDays[0] > 0 {
return periodDays[0]
}
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 360
case "total":
return productPerformancePeriodDays(map[string]any{
"kpi_date": time.Now().Format("2006-01-02"),
}, "total")
default:
return 0
}
}
func productPerformanceTotalPeriodMultiplier(periodDays float64) float64 {
if periodDays <= 0 {
periodDays = 180
}
return maxFloat(1, periodDays/180.0)
}
func productPerformanceCustomerRevenueTarget(suffix string) float64 {
switch suffix {
case "180d":