product performance grouped kpi improvements

This commit is contained in:
M_Kececi
2026-07-03 19:53:16 +03:00
parent b133d43ebe
commit a45478d15f
5 changed files with 446 additions and 83 deletions
+254 -26
View File
@@ -369,6 +369,9 @@ SET urun_ilk_grubu = '', updated_at = now()
WHERE btrim(urun_ilk_grubu) = '-'
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
`
DELETE FROM mk_product_performance_sales_daily
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
`
UPDATE mk_product_performance_sales_daily
SET askili_yan = '', updated_at = now()
WHERE btrim(askili_yan) = '-'`,
@@ -434,6 +437,18 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
`
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),
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
`
DELETE FROM mk_product_performance_price_dim
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
kpi_date DATE NOT NULL,
product_code TEXT NOT NULL,
@@ -501,6 +516,26 @@ SET urun_ilk_grubu = '', updated_at = now()
WHERE btrim(urun_ilk_grubu) = '-'
OR upper(translate(btrim(urun_ilk_grubu), 'İŞĞÜÖÇ', 'ISGUOC')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
`
DELETE FROM mk_product_performance_kpi_daily
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
`
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,
updated_at = now()
WHERE cost_price_usd > 0
AND base_price_usd > 0
AND cost_price_usd < base_price_usd`,
`
UPDATE mk_product_performance_kpi_daily
SET askili_yan = '', updated_at = now()
WHERE btrim(askili_yan) = '-'`,
@@ -719,6 +754,67 @@ func normalizeProductPerformanceVariantCodes(productCode, colorCode, yakaKodu *s
}
}
func productPerformanceColorDescriptions(ctx context.Context, colorCodes []string) map[string]string {
out := map[string]string{}
if db.MssqlDB == nil || len(colorCodes) == 0 {
return out
}
seen := map[string]bool{}
codes := make([]string, 0, len(colorCodes))
for _, raw := range colorCodes {
code := normalizeProductPerformanceCode(raw)
if code == "" || seen[code] {
continue
}
seen[code] = true
codes = append(codes, code)
}
const batchSize = 500
for start := 0; start < len(codes); start += batchSize {
end := start + batchSize
if end > len(codes) {
end = len(codes)
}
batch := codes[start:end]
placeholders := make([]string, len(batch))
args := make([]any, len(batch))
for i, code := range batch {
placeholders[i] = fmt.Sprintf("@p%d", i+1)
args[i] = code
}
query := fmt.Sprintf(`
SELECT
UPPER(LTRIM(RTRIM(ColorCode))) AS color_code,
MAX(LTRIM(RTRIM(ISNULL(ColorDescription, '')))) AS color_description
FROM cdColorDesc WITH(NOLOCK)
WHERE LangCode = 'TR'
AND UPPER(LTRIM(RTRIM(ColorCode))) IN (%s)
GROUP BY UPPER(LTRIM(RTRIM(ColorCode)))
`, strings.Join(placeholders, ","))
rows, err := db.MssqlDB.QueryContext(ctx, query, args...)
if err != nil {
log.Printf("[ProductPerformance] color descriptions failed: %v", err)
return out
}
for rows.Next() {
var code, desc string
if err := rows.Scan(&code, &desc); err != nil {
rows.Close()
log.Printf("[ProductPerformance] color description scan failed: %v", err)
return out
}
out[normalizeProductPerformanceCode(code)] = strings.TrimSpace(desc)
}
if err := rows.Err(); err != nil {
rows.Close()
log.Printf("[ProductPerformance] color description rows failed: %v", err)
return out
}
rows.Close()
}
return out
}
func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
@@ -984,6 +1080,7 @@ func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx, productPre
code = normalizeProductPerformanceProductCode(code)
askiliYan = cleanProductPerformanceOptionalAttr(askiliYan)
urunIlkGrubu = cleanProductPerformanceFirstGroup(urunIlkGrubu)
cost, usd = normalizeProductPerformanceCostPair(cost, usd)
var lp any
if lastPricing.Valid {
lp = lastPricing.Time
@@ -1063,7 +1160,7 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
}
limit := f.Limit
if limit <= 0 {
limit = 100
limit = 50000
} else if limit > 50000 {
limit = 50000
}
@@ -1103,6 +1200,7 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
}
defer rows.Close()
out := make([]models.ProductPerformanceRow, 0, limit)
colorCodes := make([]string, 0, limit)
for rows.Next() {
var r models.ProductPerformanceRow
if err := rows.Scan(
@@ -1122,9 +1220,17 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
return nil, 0, err
}
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
}
return out, total, rows.Err()
if err := rows.Err(); err != nil {
return nil, 0, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
for i := range out {
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
}
return out, total, nil
}
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
@@ -1141,6 +1247,7 @@ KPI AS (
SELECT *
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT kpi_date FROM Latest)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
),
VariantStock AS (
SELECT
@@ -1167,6 +1274,7 @@ Sales90 AS (
COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
FROM mk_product_performance_sales_daily
WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
)
SELECT
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
@@ -1195,7 +1303,7 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
return nil, err
}
if limit <= 0 {
limit = 500
limit = 50000
} else if limit > 50000 {
limit = 50000
}
@@ -1236,6 +1344,7 @@ SalesAgg AS (
COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
FROM mk_product_performance_sales_daily, Bounds
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu, market_key
),
StockAgg AS (
@@ -1287,6 +1396,7 @@ Dim AS (
1 AS src_rank
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
UNION ALL
SELECT
product_code,
@@ -1302,6 +1412,7 @@ Dim AS (
MAX(urun_alt_grubu) AS urun_alt_grubu,
2 AS src_rank
FROM mk_product_performance_sales_daily
WHERE upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu
) x
ORDER BY product_code, color_code, yaka_kodu, src_rank
@@ -1333,6 +1444,7 @@ StockOnly AS (
AND d.color_code = s.color_code
AND d.yaka_kodu = s.yaka_kodu
WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
AND upper(btrim(COALESCE(d.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
AND NOT EXISTS (
SELECT 1
FROM SalesAgg a
@@ -1356,6 +1468,7 @@ Spread AS (
COUNT(DISTINCT NULLIF(customer_code, '-'))::integer AS customer_count_total_all
FROM mk_product_performance_sales_daily, Bounds
WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu
),
Scored AS (
@@ -1463,6 +1576,7 @@ LIMIT $1
}
defer rows.Close()
out := make([]models.ProductPerformanceGeneralRow, 0, limit)
colorCodes := make([]string, 0, limit)
for rows.Next() {
var r models.ProductPerformanceGeneralRow
if err := rows.Scan(
@@ -1476,9 +1590,17 @@ LIMIT $1
return nil, err
}
r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
}
return out, rows.Err()
if err := rows.Err(); err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
for i := range out {
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
}
return out, nil
}
func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
@@ -1489,7 +1611,7 @@ func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit
return nil, err
}
if limit <= 0 {
limit = 500
limit = 50000
} else if limit > 50000 {
limit = 50000
}
@@ -1691,6 +1813,7 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
productCodes := make([]string, 0, limit)
stockKeys := make([]string, 0, limit)
colorCodes := make([]string, 0, limit)
seenProducts := map[string]bool{}
seenStockKeys := map[string]bool{}
for rows.Next() {
@@ -1704,6 +1827,7 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
}
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan)
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
if !seenProducts[r.ProductCode] {
seenProducts[r.ProductCode] = true
@@ -1731,10 +1855,15 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
if err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
filtered := out[:0]
for i := range out {
row := &out[i]
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
continue
}
if strings.TrimSpace(row.ItemDescription) == "" {
row.ItemDescription = attr.itemDescription
}
@@ -1765,9 +1894,11 @@ ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.Y
if row.OrderUSD != 0 {
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
}
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
filtered = append(filtered, *row)
}
return out, nil
return filtered, nil
}
func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
@@ -1781,8 +1912,10 @@ func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdow
if mode != "customer" {
mode = "market"
}
if limit <= 0 || limit > 1000 {
limit = 500
if limit <= 0 {
limit = 50000
} else if limit > 50000 {
limit = 50000
}
rows, err := db.MssqlDB.QueryContext(ctx, `
@@ -2015,8 +2148,10 @@ func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB
if err := EnsureProductPerformanceTables(pg); err != nil {
return nil, err
}
if limit <= 0 || limit > 1000 {
limit = 500
if limit <= 0 {
limit = 50000
} else if limit > 50000 {
limit = 50000
}
rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS (
@@ -2074,6 +2209,7 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
productCodes := make([]string, 0, limit)
stockKeys := make([]string, 0, limit)
colorCodes := make([]string, 0, limit)
seenProducts := map[string]bool{}
seenStockKeys := map[string]bool{}
for rows.Next() {
@@ -2082,6 +2218,7 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
return nil, err
}
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
if !seenProducts[r.ProductCode] {
seenProducts[r.ProductCode] = true
@@ -2108,10 +2245,15 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
if err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
filtered := out[:0]
for i := range out {
row := &out[i]
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
continue
}
if strings.TrimSpace(row.ItemDescription) == "" {
row.ItemDescription = attr.itemDescription
}
@@ -2130,9 +2272,11 @@ ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
}
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
filtered = append(filtered, *row)
}
return out, nil
return filtered, nil
}
func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
@@ -2199,6 +2343,7 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
productCodes := make([]string, 0, limit)
stockKeys := make([]string, 0, limit)
colorCodes := make([]string, 0, limit)
seenProducts := map[string]bool{}
seenStockKeys := map[string]bool{}
for rows.Next() {
@@ -2207,6 +2352,7 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
return nil, err
}
normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
if !seenProducts[r.ProductCode] {
seenProducts[r.ProductCode] = true
@@ -2229,9 +2375,19 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
if err != nil {
return nil, err
}
attrByKey, err := productPerformanceAttrLookup(ctx, pg, stockKeys)
if err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
filtered := out[:0]
for i := range out {
row := &out[i]
price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
continue
}
row.CostPriceUSD = price.cost
row.BasePriceUSD = price.base
row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
@@ -2242,17 +2398,21 @@ ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DE
row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
}
row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
filtered = append(filtered, *row)
}
return out, nil
return filtered, nil
}
func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
if err := EnsureProductPerformanceTables(pg); err != nil {
return nil, err
}
if limit <= 0 || limit > 500 {
limit = 100
if limit <= 0 {
limit = 50000
} else if limit > 50000 {
limit = 50000
}
rows, err := pg.QueryContext(ctx, `
SELECT
@@ -2277,6 +2437,7 @@ SELECT
COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY market_key
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
LIMIT $1
@@ -2306,8 +2467,10 @@ func ListProductPerformanceCountries(ctx context.Context, pg *sql.DB, limit int)
if err := EnsureProductPerformanceTables(pg); err != nil {
return nil, err
}
if limit <= 0 || limit > 500 {
limit = 100
if limit <= 0 {
limit = 50000
} else if limit > 50000 {
limit = 50000
}
rows, err := pg.QueryContext(ctx, `
SELECT
@@ -2330,6 +2493,7 @@ SELECT
COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
FROM mk_product_performance_sales_daily
WHERE sales_date >= current_date - INTERVAL '364 days'
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY customer_country, customer_segment, market_key
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
LIMIT $1
@@ -2357,8 +2521,10 @@ func ListProductPerformanceCustomers(ctx context.Context, pg *sql.DB, breakdown
if err := EnsureProductPerformanceTables(pg); err != nil {
return nil, err
}
if limit <= 0 || limit > 500 {
limit = 100
if limit <= 0 {
limit = 50000
} else if limit > 50000 {
limit = 50000
}
mode := strings.ToLower(strings.TrimSpace(breakdown))
if mode == "" {
@@ -2407,6 +2573,7 @@ SELECT
FROM mk_product_performance_sales_daily
WHERE sales_date >= current_date - INTERVAL '364 days'
AND COALESCE(customer_code,'') <> ''
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY %s
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
LIMIT $1
@@ -2437,7 +2604,7 @@ func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, break
return nil, err
}
if limit <= 0 {
limit = 500
limit = 50000
} else if limit > 50000 {
limit = 50000
}
@@ -2566,6 +2733,7 @@ WITH LatestKPI AS (
urun_alt_grubu
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
),
StockAgg AS (
@@ -2726,6 +2894,7 @@ Agg AS (
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
) s
WHERE sales_date >= DATE '2022-01-01'
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY %s
),
Enriched AS (
@@ -2913,6 +3082,7 @@ LIMIT $1
}
defer rows.Close()
out := make([]models.ProductPerformanceSalesBreakdownRow, 0, limit)
colorCodes := make([]string, 0, limit)
for rows.Next() {
var r models.ProductPerformanceSalesBreakdownRow
if err := rows.Scan(
@@ -2935,9 +3105,17 @@ LIMIT $1
); err != nil {
return nil, err
}
colorCodes = append(colorCodes, r.ColorCode)
out = append(out, r)
}
return out, rows.Err()
if err := rows.Err(); err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
for i := range out {
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
}
return out, nil
}
type ProductPerformanceGroupedRequest struct {
@@ -3135,9 +3313,24 @@ FROM (
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_total * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_total,
CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_usd_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS avg_price_usd_90d,
CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d,
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS cost_price_usd,
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_usd,
CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS base_price_try,
CASE
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
THEN SUM(CASE WHEN sales_qty_total > 0 THEN cost_price_usd * sales_qty_total WHEN stock_variant_rank = 1 THEN cost_price_usd * stock_qty ELSE 0 END)
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
ELSE 0
END AS cost_price_usd,
CASE
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_usd * sales_qty_total WHEN stock_variant_rank = 1 THEN base_price_usd * stock_qty ELSE 0 END)
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
ELSE 0
END AS base_price_usd,
CASE
WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0
THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_try * sales_qty_total WHEN stock_variant_rank = 1 THEN base_price_try * stock_qty ELSE 0 END)
/ NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0)
ELSE 0
END AS base_price_try,
COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d,
CASE WHEN SUM(sales_usd_90d) > 0 THEN SUM(gross_profit_usd_90d) / NULLIF(SUM(sales_usd_90d),0) ELSE 0 END AS gross_margin_90d,
@@ -3201,6 +3394,7 @@ func queryProductPerformanceJSONRows(ctx context.Context, pg *sql.DB, query stri
}
defer rows.Close()
out := []map[string]any{}
colorCodes := []string{}
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
@@ -3210,9 +3404,17 @@ func queryProductPerformanceJSONRows(ctx context.Context, pg *sql.DB, query stri
if err := json.Unmarshal(raw, &row); err != nil {
return nil, err
}
colorCodes = append(colorCodes, stringFromMap(row, "color_code"))
out = append(out, row)
}
return out, rows.Err()
if err := rows.Err(); err != nil {
return nil, err
}
colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
for _, row := range out {
row["color_description"] = colorDescriptions[normalizeProductPerformanceCode(stringFromMap(row, "color_code"))]
}
return out, nil
}
func productPerformanceSQLSourceCTE(mode string) string {
@@ -3312,7 +3514,8 @@ Source AS (
COALESCE(performance_bucket,'') AS performance_bucket,
COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + idleWhere + `
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')` + idleWhere + `
)`
}
@@ -3980,6 +4183,7 @@ FROM mk_product_performance_sales_daily
WHERE product_code=$1
AND color_code=$2
AND yaka_kodu=$3
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
ORDER BY sales_usd DESC, sales_date DESC
LIMIT $4
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
@@ -4108,7 +4312,7 @@ ORDER BY SizeCode
}
func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
parts := make([]string, 0, 6)
parts := []string{" AND " + productPerformanceAllowedFirstGroupSQL("urun_ilk_grubu")}
args := make([]any, 0, 6)
add := func(cond string, value any) {
args = append(args, value)
@@ -4219,6 +4423,28 @@ func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) strin
return normalizeProductPerformanceProductCode(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
}
func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD < basePriceUSD {
return basePriceUSD, costPriceUSD
}
return costPriceUSD, basePriceUSD
}
const productPerformanceExcludedFirstGroupsSQL = "('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')"
func productPerformanceAllowedFirstGroupSQL(expr string) string {
return "upper(btrim(COALESCE(" + expr + ",''))) NOT IN " + productPerformanceExcludedFirstGroupsSQL
}
func isExcludedProductPerformanceFirstGroup(value string) bool {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "MALZEMELI FASON", "MALZEMESIZ FASON", "DIGER":
return true
default:
return false
}
}
func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
out := make(map[string]productPerformancePriceInfo, len(productCodes))
if len(productCodes) == 0 {
@@ -4252,6 +4478,7 @@ WHERE product_code = ANY($1)
if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
return nil, err
}
p.cost, p.base = normalizeProductPerformanceCostPair(p.cost, p.base)
out[normalizeProductPerformanceProductCode(productCode)] = p
}
return out, rows.Err()
@@ -4280,6 +4507,7 @@ SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT kpi_date FROM Latest)
AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
`, pq.Array(variantKeys))
if err != nil {
+20 -2
View File
@@ -455,6 +455,7 @@ SalesAgg AS (
FROM mk_product_performance_sales_daily
WHERE sales_date >= DATE '2022-01-01'
AND sales_date <= $1::date
AND upper(btrim(COALESCE(urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
GROUP BY product_code, color_code, yaka_kodu, market_key
),
Scope AS (
@@ -476,7 +477,7 @@ Scope AS (
'' AS seri,
'' AS yas_grubu,
'' AS askili_yan,
'' AS urun_ilk_grubu,
COALESCE(pd.urun_ilk_grubu, '') AS urun_ilk_grubu,
'' AS urun_ana_grubu,
'' AS urun_alt_grubu,
0 AS sales_qty_30d,
@@ -494,7 +495,9 @@ Scope AS (
NULL::date AS last_sale_date,
'' AS last_ref_number
FROM LatestStock ls
LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code=ls.product_code
WHERE COALESCE(ls.stock_qty, 0) <> 0
AND upper(btrim(COALESCE(pd.urun_ilk_grubu,''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
AND NOT EXISTS (
SELECT 1
FROM SalesAgg sa
@@ -503,6 +506,20 @@ Scope AS (
AND sa.yaka_kodu = ls.yaka_kodu
)
),
PriceDim AS (
SELECT
product_code,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN base_price_usd
ELSE cost_price_usd
END AS cost_price_usd,
CASE
WHEN cost_price_usd > 0 AND base_price_usd > 0 AND cost_price_usd < base_price_usd THEN cost_price_usd
ELSE base_price_usd
END AS base_price_usd,
base_price_try
FROM mk_product_performance_price_dim
),
Base AS (
SELECT
s.*,
@@ -538,7 +555,8 @@ Base AS (
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 mk_product_performance_price_dim pd ON pd.product_code=s.product_code
LEFT JOIN PriceDim pd ON pd.product_code=s.product_code
WHERE upper(btrim(COALESCE(NULLIF(s.urun_ilk_grubu,''), pd.urun_ilk_grubu, ''))) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')
),
Spread AS (
SELECT