|
|
|
@@ -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 {
|
|
|
|
@@ -2945,6 +3123,7 @@ type ProductPerformanceGroupedRequest struct {
|
|
|
|
|
GroupLevels []string
|
|
|
|
|
ExpandedKeys map[string]bool
|
|
|
|
|
Limit int
|
|
|
|
|
MainGroup string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
|
|
|
|
@@ -2976,17 +3155,26 @@ type productPerformanceSQLGroupFilter struct {
|
|
|
|
|
|
|
|
|
|
func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
|
|
|
|
|
mode := strings.TrimSpace(req.Mode)
|
|
|
|
|
if mode != "products" && mode != "idle" {
|
|
|
|
|
if mode != "products" && mode != "product_detail" && mode != "idle" {
|
|
|
|
|
return nil, false, nil
|
|
|
|
|
}
|
|
|
|
|
if err := EnsureProductPerformanceTables(pg); err != nil {
|
|
|
|
|
return nil, true, err
|
|
|
|
|
}
|
|
|
|
|
out := make([]map[string]any, 0, 512)
|
|
|
|
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, nil, req.ExpandedKeys, req.Limit)
|
|
|
|
|
filters := productPerformanceGroupedBaseFilters(req)
|
|
|
|
|
err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.Limit)
|
|
|
|
|
return out, true, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
|
|
|
|
|
mainGroup := strings.TrimSpace(req.MainGroup)
|
|
|
|
|
if strings.TrimSpace(req.Mode) != "product_detail" || mainGroup == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return []productPerformanceSQLGroupFilter{{Field: "urun_ana_grubu", Value: mainGroup}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, limit int) error {
|
|
|
|
|
if level >= len(levels) {
|
|
|
|
|
rows, err := queryProductPerformanceSQLLeafRows(ctx, pg, mode, filters, limit)
|
|
|
|
@@ -3135,9 +3323,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 +3404,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 +3414,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 +3524,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 + `
|
|
|
|
|
)`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -3745,6 +3958,8 @@ func defaultProductPerformanceGroupLevels(mode string) []string {
|
|
|
|
|
switch mode {
|
|
|
|
|
case "sales_color_yaka_market_customer":
|
|
|
|
|
return []string{"color_code", "yaka_kodu", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "country", "market_key", "customer_segment", "customer_code", "customer_name"}
|
|
|
|
|
case "product_detail":
|
|
|
|
|
return []string{"urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "market_key"}
|
|
|
|
|
case "idle":
|
|
|
|
|
return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu"}
|
|
|
|
|
case "sales_product_country_segment_market_customer":
|
|
|
|
@@ -3980,6 +4195,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 +4324,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 +4435,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 +4490,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 +4519,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 {
|
|
|
|
|