Compare commits

...
74 Commits
Author SHA1 Message Date
M_Kececi db13574e9d Fix product performance grouped JSON build 2026-08-19 01:10:20 +03:00
M_Kececi ba9a744aef Fix product performance grouped JSON build 2026-08-19 01:06:55 +03:00
M_Kececi 82ad3783b3 Fix product performance grouped JSON build 2026-08-19 01:00:03 +03:00
M_Kececi 322bed753f Fix product performance grouped JSON build 2026-08-19 00:53:51 +03:00
M_Kececi bdc9459330 Fix product performance grouped JSON build 2026-08-19 00:50:40 +03:00
M_Kececi 2d85a33623 Fix product performance grouped JSON build 2026-08-19 00:45:58 +03:00
M_Kececi 5f1c37859b Fix product performance grouped JSON build 2026-08-19 00:08:19 +03:00
M_Kececi 31a71ab566 Fix product performance grouped JSON build 2026-08-18 23:38:24 +03:00
M_Kececi 25ef023df5 Fix product performance grouped JSON build 2026-08-18 23:32:39 +03:00
M_Kececi 0c51729f7a Fix product performance grouped JSON build 2026-08-18 22:41:08 +03:00
M_Kececi 20f5fea2e8 Fix product performance grouped JSON build 2026-08-18 22:35:20 +03:00
M_Kececi 65e8aa2b62 Fix product performance grouped JSON build 2026-08-18 22:23:41 +03:00
M_Kececi 1f8355b77c Fix product performance grouped JSON build 2026-08-18 22:17:28 +03:00
M_Kececi 11048f1555 Fix product performance grouped JSON build 2026-08-18 22:11:13 +03:00
M_Kececi b83a3d22ce Fix product performance grouped JSON build 2026-08-18 22:07:33 +03:00
M_Kececi 77af9ef533 Fix product performance grouped JSON build 2026-08-18 22:02:44 +03:00
M_Kececi 4395d6bae2 Fix product performance grouped JSON build 2026-08-18 21:57:46 +03:00
M_Kececi 6bc19e7a43 Fix product performance grouped JSON build 2026-08-18 21:54:16 +03:00
M_Kececi 797d069e59 Fix product performance grouped JSON build 2026-08-18 21:47:27 +03:00
M_Kececi 36e0ec9ff5 Fix product performance grouped JSON build 2026-08-18 21:39:53 +03:00
M_Kececi f14a96a024 Fix product performance grouped JSON build 2026-08-18 21:35:10 +03:00
M_Kececi f877b611b7 Fix product performance grouped JSON build 2026-08-18 21:13:38 +03:00
M_Kececi dbb3f3f380 Fix product performance grouped JSON build 2026-08-18 21:07:52 +03:00
M_Kececi 03287d4850 Fix product performance grouped JSON build 2026-08-18 20:58:29 +03:00
M_Kececi 12ce3a3eca Fix product performance grouped JSON build 2026-08-18 20:54:45 +03:00
M_Kececi 52420bc5ff Fix product performance grouped JSON build 2026-08-18 20:40:39 +03:00
M_Kececi d5aa43ccb5 Fix product performance grouped JSON build 2026-08-18 20:30:44 +03:00
M_Kececi 04461acb58 Fix product performance grouped JSON build 2026-08-18 20:23:55 +03:00
M_Kececi cd101811c4 Fix product performance grouped JSON build 2026-08-18 20:19:24 +03:00
M_Kececi c4028010c0 Fix product performance grouped JSON build 2026-08-18 20:13:21 +03:00
M_Kececi c2d010751f Fix product performance grouped JSON build 2026-08-18 20:08:09 +03:00
M_Kececi a9ab9fd9b0 Fix product performance grouped JSON build 2026-08-18 20:03:12 +03:00
M_Kececi 1945fd4456 Fix product performance grouped JSON build 2026-08-18 19:57:24 +03:00
M_Kececi 7b8f8905e5 Fix product performance grouped JSON build 2026-08-18 19:44:36 +03:00
M_Kececi 89df3d2d3d Fix product performance grouped JSON build 2026-08-18 18:44:26 +03:00
M_Kececi 474904481f Fix product performance grouped JSON build 2026-08-18 13:38:32 +03:00
M_Kececi 6eaf0e3ed3 Fix product performance grouped JSON build 2026-08-18 13:23:10 +03:00
M_Kececi 8249761415 Fix product performance grouped JSON build 2026-08-18 13:10:11 +03:00
M_Kececi dbd84f7399 Fix product performance grouped JSON build 2026-08-18 13:04:50 +03:00
M_Kececi 9edc806345 Fix product performance grouped JSON build 2026-08-18 12:45:11 +03:00
M_Kececi 7a7008cc1b Fix product performance grouped JSON build 2026-07-14 00:18:23 +03:00
M_Kececi 3e0253f6be Fix product performance grouped JSON build 2026-07-13 22:43:51 +03:00
M_Kececi b80b5024f4 Fix product performance grouped JSON build 2026-07-13 21:55:17 +03:00
M_Kececi 5eaf8be9e6 Fix product performance grouped JSON build 2026-07-12 16:15:41 +03:00
M_Kececi cc2930caf2 Fix product performance grouped JSON build 2026-07-09 16:41:41 +03:00
M_Kececi 26b4a63c45 Fix product performance grouped JSON build 2026-07-07 23:51:29 +03:00
M_Kececi c6a3d1552e Fix product performance grouped JSON build 2026-07-07 12:59:19 +03:00
M_Kececi 005d2eafac Fix product performance grouped JSON build 2026-07-07 01:21:07 +03:00
M_Kececi 289f204752 Fix product performance grouped JSON build 2026-07-06 18:01:34 +03:00
M_Kececi 1448dab9d7 Fix product performance grouped JSON build 2026-07-06 17:01:39 +03:00
M_Kececi 5d4216d42e Fix product performance grouped JSON build 2026-07-06 16:54:36 +03:00
M_Kececi 3993c363cb Fix product performance grouped JSON build 2026-07-06 01:28:44 +03:00
M_Kececi a0da3f2bed Fix product performance grouped JSON build 2026-07-06 00:53:30 +03:00
M_Kececi 19a42e0551 Merge remote-tracking branch 'origin/master' 2026-07-05 23:43:20 +03:00
M_Kececi 53cdd1a4a1 Fix product performance grouped JSON build 2026-07-05 23:43:17 +03:00
M_Kececi fe29e763c5 Fix product performance grouped JSON build 2026-07-05 23:41:26 +03:00
M_Kececi 9e1ecb9b3f Fix product performance grouped JSON build 2026-07-05 22:06:43 +03:00
M_Kececi fbdde54eee Fix product performance grouped JSON build 2026-07-05 20:31:27 +03:00
M_Kececi b2113a8d49 Fix product performance grouped JSON build 2026-07-05 18:31:40 +03:00
M_Kececi 28e33d5e58 Fix product performance grouped JSON build 2026-07-05 17:02:28 +03:00
M_Kececi fbdc4934ff Merge remote-tracking branch 'origin/master' 2026-07-05 15:30:29 +03:00
M_Kececi 5b512fd30a Fix product performance grouped JSON build 2026-07-05 15:30:25 +03:00
M_Kececi 7879172c54 Fix product performance grouped JSON build 2026-07-05 15:29:59 +03:00
M_Kececi c602e1e119 Fix product performance grouped JSON build 2026-07-05 13:54:52 +03:00
M_Kececi 27934bccdf Fix product performance grouped JSON build 2026-07-05 05:16:31 +03:00
M_Kececi 48bde09c2c Fix product performance grouped JSON build 2026-07-05 02:58:06 +03:00
M_Kececi 480cad0d6f Fix product performance grouped JSON build 2026-07-05 01:02:04 +03:00
M_Kececi 9f6e288d04 Fix product performance grouped JSON build 2026-07-05 00:38:44 +03:00
M_Kececi d0717900a6 Fix product performance grouped JSON build 2026-07-04 23:31:45 +03:00
M_Kececi 25bc9b2d02 Fix product performance grouped JSON build 2026-07-04 14:11:19 +03:00
M_Kececi 4c56062ef4 Fix product performance grouped JSON build 2026-07-04 13:45:43 +03:00
M_Kececi 60b2f76537 Fix product performance grouped JSON build 2026-07-04 13:26:10 +03:00
M_Kececi 81384426ff Fix product performance grouped JSON build 2026-07-04 13:10:34 +03:00
M_Kececi 3697e35064 Fix product performance grouped JSON build 2026-07-03 21:35:14 +03:00
25 changed files with 8013 additions and 1291 deletions
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="GoModuleSettings" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
+2
View File
@@ -3,6 +3,8 @@ bssapp
# Env
.env
.env.local
.env.argebaggi.local
mail.env
# Runtime fonts
+2 -1
View File
@@ -66,10 +66,11 @@ func main() {
log.Fatalf("refresh failed: %v", err)
}
fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d\n",
fmt.Printf("product performance refresh done: sales=%d stock=%d kpi=%d snapshots=%d\n",
result.SalesRows,
result.StockRows,
result.KpiRows,
result.SnapshotRows,
)
}
+39
View File
@@ -14,6 +14,7 @@ import (
var MssqlDB *sql.DB
var UretimDB *sql.DB
var ArgeBaggiDB *sql.DB
func envInt(name string, fallback int) int {
raw := strings.TrimSpace(os.Getenv(name))
@@ -158,3 +159,41 @@ func ConnectMSSQLUretim() error {
func GetUretimDB() *sql.DB {
return UretimDB
}
// ConnectMSSQLArgeBaggi initializes the optional ArgeBAGGI connection.
// Credentials must be supplied through ARGEBAGGI_MSSQL_CONN; never keep them
// in source control.
func ConnectMSSQLArgeBaggi() error {
connString := strings.TrimSpace(os.Getenv("ARGEBAGGI_MSSQL_CONN"))
if connString == "" {
return fmt.Errorf("ARGEBAGGI_MSSQL_CONN tanimli degil")
}
connectionTimeoutSec := envInt("ARGEBAGGI_MSSQL_CONNECTION_TIMEOUT_SEC", 10)
dialTimeoutSec := envInt("ARGEBAGGI_MSSQL_DIAL_TIMEOUT_SEC", connectionTimeoutSec)
connString = ensureMSSQLTimeouts(connString, connectionTimeoutSec, dialTimeoutSec)
var err error
ArgeBaggiDB, err = sql.Open("sqlserver", connString)
if err != nil {
return fmt.Errorf("ArgeBAGGI MSSQL baglanti hatasi: %w", err)
}
ArgeBaggiDB.SetMaxOpenConns(envInt("ARGEBAGGI_MSSQL_MAX_OPEN_CONNS", 10))
ArgeBaggiDB.SetMaxIdleConns(envInt("ARGEBAGGI_MSSQL_MAX_IDLE_CONNS", 5))
ArgeBaggiDB.SetConnMaxLifetime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_LIFETIME_MIN", 30)) * time.Minute)
ArgeBaggiDB.SetConnMaxIdleTime(time.Duration(envInt("ARGEBAGGI_MSSQL_CONN_MAX_IDLE_MIN", 10)) * time.Minute)
if err = ArgeBaggiDB.Ping(); err != nil {
_ = ArgeBaggiDB.Close()
ArgeBaggiDB = nil
return fmt.Errorf("ArgeBAGGI MSSQL erisilemiyor: %w", err)
}
fmt.Printf("ArgeBAGGI MSSQL baglantisi basarili (connection timeout=%ds, dial timeout=%ds)\n", connectionTimeoutSec, dialTimeoutSec)
return nil
}
func GetArgeBaggiDB() *sql.DB {
return ArgeBaggiDB
}
+41
View File
@@ -227,6 +227,7 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter()
mountUploads(r)
mountSPA(r)
/*
@@ -939,6 +940,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
wrapV3(routes.GetProductPerformanceHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/export-excel", "POST",
"pricing", "view",
wrapV3(routes.ExportProductPerformanceExcelHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/summary", "GET",
"pricing", "view",
@@ -999,6 +1005,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/grouped/filter-options", "POST",
"pricing", "view",
wrapV3(routes.GetProductPerformanceGroupedFilterOptionsHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/pricing/product-performance/sales-details", "GET",
"pricing", "view",
@@ -1379,6 +1390,7 @@ func main() {
// (e.g. SSH-tunnel PostgreSQL on 127.0.0.1:15432).
if runtime.GOOS == "windows" {
_ = godotenv.Overload(".env.local")
_ = godotenv.Overload(".env.argebaggi.local")
}
jwtSecret := os.Getenv("JWT_SECRET")
@@ -1406,6 +1418,15 @@ func main() {
log.Fatal(err)
}
}
if err := db.ConnectMSSQLArgeBaggi(); err != nil {
if strings.Contains(err.Error(), "ARGEBAGGI_MSSQL_CONN tanimli degil") {
log.Println("ArgeBAGGI DB baglantisi atlandi: ARGEBAGGI_MSSQL_CONN tanimli degil")
} else {
// This source enriches one screen; a temporary outage must not stop
// the whole application from starting.
log.Println("ArgeBAGGI DB baglantisi basarisiz:", err)
}
}
pgDB, err := db.ConnectPostgres()
if err != nil {
@@ -1507,6 +1528,26 @@ func main() {
}
func mountUploads(r *mux.Router) {
root := strings.TrimSpace(os.Getenv("BLOB_ROOT"))
if root == "" {
return
}
uploadsRoot := filepath.Join(root, "uploads")
if fi, err := os.Stat(uploadsRoot); err != nil || !fi.IsDir() {
return
}
fileServer := http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadsRoot)))
r.PathPrefix("/uploads/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Cache-Control", "public, max-age=3600")
fileServer.ServeHTTP(w, r)
})).Methods(http.MethodGet, http.MethodHead, http.MethodOptions)
}
func mountSPA(r *mux.Router) {
r.NotFoundHandler = http.HandlerFunc(spaIndex)
r.HandleFunc("/", spaIndex).Methods(http.MethodGet)
+174 -138
View File
@@ -1,68 +1,85 @@
package models
type ProductPerformanceRow struct {
KpiDate string `json:"kpi_date"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
StockQty float64 `json:"stock_qty"`
SalesQty30 float64 `json:"sales_qty_30d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
StockDays90 float64 `json:"stock_days_90d"`
StockDays180 float64 `json:"stock_days_180d"`
StockDays365 float64 `json:"stock_days_365d"`
StockDaysTotal float64 `json:"stock_days_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
BasePriceTRY float64 `json:"base_price_try"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"`
GrossMargin90 float64 `json:"gross_margin_90d"`
GrossMargin180 float64 `json:"gross_margin_180d"`
UnitProfitCost90 float64 `json:"unit_profit_cost_90d"`
UnitProfitCost180 float64 `json:"unit_profit_cost_180d"`
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
MarketCount90 int `json:"market_count_90d"`
CustomerCount90 int `json:"customer_count_90d"`
SalesIndex90 float64 `json:"sales_index_90d"`
PriceIndex90 float64 `json:"price_index_90d"`
MarginIndex90 float64 `json:"margin_index_90d"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
UpdatedAt string `json:"updated_at"`
KpiDate string `json:"kpi_date"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
Seri string `json:"seri"`
YasGrubu string `json:"yas_grubu"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
StockQty float64 `json:"stock_qty"`
SalesQty30 float64 `json:"sales_qty_30d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesQty730 float64 `json:"sales_qty_730d"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSD30 float64 `json:"sales_usd_30d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgDailySales90 float64 `json:"avg_daily_sales_90d"`
AvgDailySales180 float64 `json:"avg_daily_sales_180d"`
AvgDailySales365 float64 `json:"avg_daily_sales_365d"`
AvgDailySalesTotal float64 `json:"avg_daily_sales_total"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockDays90 float64 `json:"stock_days_90d"`
StockDays180 float64 `json:"stock_days_180d"`
StockDays365 float64 `json:"stock_days_365d"`
StockDaysTotal float64 `json:"stock_days_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
CostPriceUSD float64 `json:"cost_price_usd"`
BasePriceUSD float64 `json:"base_price_usd"`
BasePriceTRY float64 `json:"base_price_try"`
GrossProfitUSD90 float64 `json:"gross_profit_usd_90d"`
GrossProfitUSD180 float64 `json:"gross_profit_usd_180d"`
GrossMargin90 float64 `json:"gross_margin_90d"`
GrossMargin180 float64 `json:"gross_margin_180d"`
UnitProfitCost90 float64 `json:"unit_profit_cost_90d"`
UnitProfitCost180 float64 `json:"unit_profit_cost_180d"`
UnitProfitBase90 float64 `json:"unit_profit_base_90d"`
UnitProfitBase180 float64 `json:"unit_profit_base_180d"`
MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"`
CustomerCount180 int `json:"customer_count_180d"`
CustomerCount365 int `json:"customer_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
PriceIndex90 float64 `json:"price_index_90d"`
MarginIndex90 float64 `json:"margin_index_90d"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
LastRefNumber string `json:"last_ref_number"`
UpdatedAt string `json:"updated_at"`
}
type ProductPerformanceSummary struct {
@@ -308,82 +325,101 @@ type ProductPerformanceCustomerRow struct {
}
type ProductPerformanceSalesBreakdownRow struct {
Breakdown string `json:"breakdown"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
CustomerCount90 int `json:"customer_count_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
CustomerCount180 int `json:"customer_count_180d"`
InvoiceCount180 int `json:"invoice_count_180d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
CustomerCount365 int `json:"customer_count_365d"`
InvoiceCount365 int `json:"invoice_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
InvoiceCountTotal int `json:"invoice_count_total"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
StockQty float64 `json:"stock_qty"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
HasCost bool `json:"has_cost"`
SalesIndex90 float64 `json:"sales_index_90d"`
CustomerScore90 float64 `json:"customer_score_90d"`
CustomerScore180 float64 `json:"customer_score_180d"`
CustomerScore365 float64 `json:"customer_score_365d"`
CustomerScoreTotal float64 `json:"customer_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
Breakdown string `json:"breakdown"`
ProductCode string `json:"product_code"`
ColorCode string `json:"color_code"`
ColorDescription string `json:"color_description"`
YakaKodu string `json:"yaka_kodu"`
ItemDescription string `json:"item_description"`
Kategori string `json:"kategori"`
AskiliYan string `json:"askili_yan"`
UrunIlkGrubu string `json:"urun_ilk_grubu"`
UrunAnaGrubu string `json:"urun_ana_grubu"`
UrunAltGrubu string `json:"urun_alt_grubu"`
MarketKey string `json:"market_key"`
Country string `json:"country"`
CustomerSegment string `json:"customer_segment"`
CustomerCode string `json:"customer_code"`
CustomerName string `json:"customer_name"`
ProductCount int `json:"product_count"`
ProductGroupCount90 int `json:"product_group_count_90d"`
ProductGroupCount180 int `json:"product_group_count_180d"`
ProductGroupCount365 int `json:"product_group_count_365d"`
ProductGroupCountTotal int `json:"product_group_count_total"`
MarketCount90 int `json:"market_count_90d"`
MarketCount180 int `json:"market_count_180d"`
MarketCount365 int `json:"market_count_365d"`
MarketCountTotal int `json:"market_count_total"`
CustomerCount90 int `json:"customer_count_90d"`
InvoiceCount90 int `json:"invoice_count_90d"`
SalesQty90 float64 `json:"sales_qty_90d"`
SalesUSD90 float64 `json:"sales_usd_90d"`
AvgPriceUSD90 float64 `json:"avg_price_usd_90d"`
BasePriceUSD90 float64 `json:"base_price_usd_90d"`
CostPriceUSD90 float64 `json:"cost_price_usd_90d"`
GrossProfitBase90 float64 `json:"gross_profit_base_usd_90d"`
GrossProfitCost90 float64 `json:"gross_profit_cost_usd_90d"`
GrossMarginBase90 float64 `json:"gross_margin_base_90d"`
GrossMarginCost90 float64 `json:"gross_margin_cost_90d"`
CustomerCount180 int `json:"customer_count_180d"`
InvoiceCount180 int `json:"invoice_count_180d"`
SalesQty180 float64 `json:"sales_qty_180d"`
SalesUSD180 float64 `json:"sales_usd_180d"`
AvgPriceUSD180 float64 `json:"avg_price_usd_180d"`
BasePriceUSD180 float64 `json:"base_price_usd_180d"`
CostPriceUSD180 float64 `json:"cost_price_usd_180d"`
GrossProfitBase180 float64 `json:"gross_profit_base_usd_180d"`
GrossProfitCost180 float64 `json:"gross_profit_cost_usd_180d"`
GrossMarginBase180 float64 `json:"gross_margin_base_180d"`
GrossMarginCost180 float64 `json:"gross_margin_cost_180d"`
SalesQty365 float64 `json:"sales_qty_365d"`
SalesUSD365 float64 `json:"sales_usd_365d"`
AvgPriceUSD365 float64 `json:"avg_price_usd_365d"`
BasePriceUSD365 float64 `json:"base_price_usd_365d"`
CostPriceUSD365 float64 `json:"cost_price_usd_365d"`
GrossProfitBase365 float64 `json:"gross_profit_base_usd_365d"`
GrossProfitCost365 float64 `json:"gross_profit_cost_usd_365d"`
GrossMarginBase365 float64 `json:"gross_margin_base_365d"`
GrossMarginCost365 float64 `json:"gross_margin_cost_365d"`
CustomerCount365 int `json:"customer_count_365d"`
InvoiceCount365 int `json:"invoice_count_365d"`
CustomerCountTotal int `json:"customer_count_total"`
InvoiceCountTotal int `json:"invoice_count_total"`
SalesQtyTotal float64 `json:"sales_qty_total"`
SalesUSDTotal float64 `json:"sales_usd_total"`
AvgPriceUSDTotal float64 `json:"avg_price_usd_total"`
BasePriceUSDTotal float64 `json:"base_price_usd_total"`
CostPriceUSDTotal float64 `json:"cost_price_usd_total"`
GrossProfitBaseTotal float64 `json:"gross_profit_base_usd_total"`
GrossProfitCostTotal float64 `json:"gross_profit_cost_usd_total"`
GrossMarginBaseTotal float64 `json:"gross_margin_base_total"`
GrossMarginCostTotal float64 `json:"gross_margin_cost_total"`
StockQty float64 `json:"stock_qty"`
AvgStock90 float64 `json:"avg_stock_90d"`
AvgStock180 float64 `json:"avg_stock_180d"`
AvgStock365 float64 `json:"avg_stock_365d"`
AvgStockTotal float64 `json:"avg_stock_total"`
StockTurnover90 float64 `json:"stock_turnover_90d"`
StockTurnover180 float64 `json:"stock_turnover_180d"`
StockTurnover365 float64 `json:"stock_turnover_365d"`
StockTurnoverTotal float64 `json:"stock_turnover_total"`
HasCost bool `json:"has_cost"`
SalesIndex90 float64 `json:"sales_index_90d"`
SalesIndex180 float64 `json:"sales_index_180d"`
SalesIndex365 float64 `json:"sales_index_365d"`
SalesIndexTotal float64 `json:"sales_index_total"`
CustomerScore90 float64 `json:"customer_score_90d"`
CustomerScore180 float64 `json:"customer_score_180d"`
CustomerScore365 float64 `json:"customer_score_365d"`
CustomerScoreTotal float64 `json:"customer_score_total"`
PerformanceScore90 float64 `json:"performance_score_90d"`
PerformanceScore180 float64 `json:"performance_score_180d"`
PerformanceScore365 float64 `json:"performance_score_365d"`
PerformanceScoreTotal float64 `json:"performance_score_total"`
PerformanceScore float64 `json:"performance_score"`
PerformanceBucket string `json:"performance_bucket"`
Recommendation string `json:"recommendation"`
LastSaleDate string `json:"last_sale_date"`
}
type ProductPerformanceSalesDetailRow struct {
+19 -12
View File
@@ -1,22 +1,29 @@
package models
type ProductionNoCostProductRow struct {
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
IslemTarihi string `json:"dteIslemTarihi"`
FirmaKodu string `json:"FirmaKodu"`
FirmaAdi string `json:"FirmaAdi"`
SonIsEmriVeren string `json:"SonIsEmriVeren"`
ModelAdi string `json:"sAdi"`
Kodu string `json:"sKodu"`
SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"`
MModelKodu string `json:"sMModelKodu"`
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
IslemTarihi string `json:"dteIslemTarihi"`
FirmaKodu string `json:"FirmaKodu"`
FirmaAdi string `json:"FirmaAdi"`
SonIsEmriVeren string `json:"SonIsEmriVeren"`
ModelAdi string `json:"sAdi"`
Kodu string `json:"sKodu"`
SKullaniciAdi string `json:"sKullaniciAdi"`
SKullaniciGunc string `json:"sKullaniciAdiGunc"`
MMiktarG float64 `json:"lMMiktar_G"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
MModelKodu string `json:"sMModelKodu"`
}
type ProductionHasCostProductRow struct {
UretimSekli string `json:"UretimSekli"`
UrtSiparisNo string `json:"nUrtSiparisNo"`
CeketDepoGiris float64 `json:"ceketDepoGiris"`
PantolonDepoGiris float64 `json:"pantolonDepoGiris"`
YelekDepoGiris float64 `json:"yelekDepoGiris"`
NOnMLNo string `json:"nOnMLNo"`
UrunKodu string `json:"UrunKodu"`
UrunAdi string `json:"UrunAdi"`
+94 -2
View File
@@ -26,6 +26,12 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
deltaHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_DELTA_HHMM", "02:00")
deltaTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_DELTA_TIMEOUT_HOURS", 6, 1, 24)
runOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_DELTA_RUN_ON_STARTUP", false)
fullEnabled := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_ENABLED", false)
fullWeekday := productPerformanceEnvWeekday("PRODUCT_PERFORMANCE_FULL_WEEKDAY", time.Saturday)
fullHHMM := productPerformanceEnvString("PRODUCT_PERFORMANCE_FULL_HHMM", "16:00")
fullStartDate := productPerformanceEnvDate("PRODUCT_PERFORMANCE_FULL_START_DATE", time.Date(2022, 1, 1, 0, 0, 0, 0, time.Local))
fullTimeoutHours := productPerformanceEnvInt("PRODUCT_PERFORMANCE_FULL_TIMEOUT_HOURS", 18, 1, 72)
fullRunOnStartup := productPerformanceEnvBool("PRODUCT_PERFORMANCE_FULL_RUN_ON_STARTUP", false)
var running int32
runDelta := func(reason string) {
@@ -49,12 +55,40 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.DurationMS)
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
runFull := func(reason string) {
if !atomic.CompareAndSwapInt32(&running, 0, 1) {
log.Printf("[ProductPerformanceJob] skip (%s): already running", reason)
return
}
defer atomic.StoreInt32(&running, 0)
endDate := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(fullTimeoutHours)*time.Hour)
defer cancel()
result, err := queries.RefreshProductPerformance(ctx, pgDB, queries.ProductPerformanceRefreshRequest{
Mode: "full",
Stage: "all",
StartDate: fullStartDate,
EndDate: endDate,
})
if err != nil {
log.Printf("[ProductPerformanceJob] error (%s): %v", reason, err)
return
}
log.Printf("[ProductPerformanceJob] ok (%s): mode=%s stage=%s start=%s end=%s sales=%d stock=%d kpi=%d snapshots=%d duration_ms=%d",
reason, result.Mode, result.Stage, result.StartDate, result.EndDate, result.SalesRows, result.StockRows, result.KpiRows, result.SnapshotRows, result.DurationMS)
}
log.Printf("[ProductPerformanceJob] scheduled daily_delta=%s lookback_days=%d run_delta_on_startup=%t",
deltaHHMM, deltaDays, runOnStartup)
if fullEnabled {
log.Printf("[ProductPerformanceJob] scheduled weekly_full=%s %s start_date=%s timeout_hours=%d run_full_on_startup=%t",
fullWeekday.String(), fullHHMM, fullStartDate.Format("2006-01-02"), fullTimeoutHours, fullRunOnStartup)
}
go func() {
if runOnStartup {
@@ -69,6 +103,21 @@ func startProductPerformanceScheduler(pgDB *sql.DB) {
runDelta("daily-delta")
}
}()
if fullEnabled {
go func() {
if fullRunOnStartup {
time.Sleep(30 * time.Second)
runFull("startup-full")
}
for {
next := productPerformanceNextWeekly(time.Now(), fullWeekday, fullHHMM)
log.Printf("[ProductPerformanceJob] weekly full next_at=%s in=%s", next.Format(time.RFC3339), time.Until(next).Round(time.Second))
time.Sleep(time.Until(next))
runFull("weekly-full")
}
}()
}
}
func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
@@ -80,6 +129,16 @@ func productPerformanceNextDaily(now time.Time, hhmm string) time.Time {
return next
}
func productPerformanceNextWeekly(now time.Time, weekday time.Weekday, hhmm string) time.Time {
hour, minute := productPerformanceParseHHMM(hhmm, 16, 0)
daysUntil := (int(weekday) - int(now.Weekday()) + 7) % 7
next := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, now.Location()).AddDate(0, 0, daysUntil)
if !next.After(now) {
next = next.AddDate(0, 0, 7)
}
return next
}
func productPerformanceParseHHMM(raw string, fallbackHour, fallbackMinute int) (int, int) {
parts := strings.Split(strings.TrimSpace(raw), ":")
if len(parts) != 2 {
@@ -120,3 +179,36 @@ func productPerformanceEnvBool(name string, fallback bool) bool {
}
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productPerformanceEnvDate(name string, fallback time.Time) time.Time {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return fallback
}
parsed, err := time.ParseInLocation("2006-01-02", raw, time.Local)
if err != nil {
return fallback
}
return parsed
}
func productPerformanceEnvWeekday(name string, fallback time.Weekday) time.Weekday {
switch strings.ToLower(strings.TrimSpace(os.Getenv(name))) {
case "sunday", "sun", "pazar", "0":
return time.Sunday
case "monday", "mon", "pazartesi", "1":
return time.Monday
case "tuesday", "tue", "sali", "salı", "2":
return time.Tuesday
case "wednesday", "wed", "carsamba", "çarşamba", "3":
return time.Wednesday
case "thursday", "thu", "persembe", "perÅŸembe", "4":
return time.Thursday
case "friday", "fri", "cuma", "5":
return time.Friday
case "saturday", "sat", "cumartesi", "6":
return time.Saturday
default:
return fallback
}
}
+3257 -277
View File
@@ -8,7 +8,10 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
@@ -39,16 +42,19 @@ type ProductPerformanceRefreshRequest struct {
}
type ProductPerformanceRefreshResult struct {
Mode string `json:"mode"`
Stage string `json:"stage"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
SalesRows int `json:"sales_rows"`
StockRows int `json:"stock_rows"`
KpiRows int `json:"kpi_rows"`
DurationMS int64 `json:"duration_ms"`
Mode string `json:"mode"`
Stage string `json:"stage"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
SalesRows int `json:"sales_rows"`
StockRows int `json:"stock_rows"`
KpiRows int `json:"kpi_rows"`
SnapshotRows int `json:"snapshot_rows"`
DurationMS int64 `json:"duration_ms"`
}
type productPerformanceSnapshotBypassKey struct{}
func productPerformanceStockQuery() string {
return `
;WITH ActiveWarehouses AS (
@@ -367,10 +373,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
UPDATE mk_product_performance_sales_daily
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')`,
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) 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')`,
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')`,
`
UPDATE mk_product_performance_sales_daily
SET askili_yan = '', updated_at = now()
@@ -439,15 +445,15 @@ 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(btrim(COALESCE(urun_ilk_grubu,''))) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'DIGER')`,
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')`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
kpi_date DATE NOT NULL,
@@ -475,6 +481,10 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
sales_usd_365d NUMERIC(18,4) NOT NULL DEFAULT 0,
avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0,
stock_days_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
@@ -491,8 +501,17 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
market_count_90d INTEGER NOT NULL DEFAULT 0,
market_count_180d INTEGER NOT NULL DEFAULT 0,
market_count_365d INTEGER NOT NULL DEFAULT 0,
market_count_total INTEGER NOT NULL DEFAULT 0,
customer_count_90d INTEGER NOT NULL DEFAULT 0,
customer_count_180d INTEGER NOT NULL DEFAULT 0,
customer_count_365d INTEGER NOT NULL DEFAULT 0,
customer_count_total INTEGER NOT NULL DEFAULT 0,
sales_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0,
sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0,
price_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
margin_index_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
performance_score NUMERIC(18,6) NOT NULL DEFAULT 0,
@@ -514,27 +533,27 @@ CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
UPDATE mk_product_performance_kpi_daily
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')`,
OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) 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')`,
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')`,
`
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()
@@ -546,6 +565,10 @@ WHERE btrim(askili_yan) = '-'`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
@@ -561,6 +584,62 @@ WHERE btrim(askili_yan) = '-'`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_180d INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_365d INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_total INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_180d INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_365d INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_total INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot (
report_key TEXT NOT NULL,
row_order INTEGER NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_mk_product_performance_report_snapshot PRIMARY KEY (report_key, row_order)
)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_report_snapshot_key ON mk_product_performance_report_snapshot (report_key, row_order)`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot_meta (
report_key TEXT PRIMARY KEY,
row_count INTEGER NOT NULL DEFAULT 0,
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
duration_ms BIGINT NOT NULL DEFAULT 0
)`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot (
report_key TEXT NOT NULL,
row_order INTEGER NOT NULL,
mode TEXT NOT NULL,
group_levels_key TEXT NOT NULL,
main_group TEXT NOT NULL DEFAULT '',
group_level INTEGER NOT NULL DEFAULT 0,
group_key TEXT NOT NULL DEFAULT '',
parent_key TEXT NOT NULL DEFAULT '',
group_field TEXT NOT NULL DEFAULT '',
group_value TEXT NOT NULL DEFAULT '',
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT pk_mk_product_performance_grouped_snapshot PRIMARY KEY (report_key, row_order)
)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_field ON mk_product_performance_grouped_snapshot (report_key, group_field, group_value)`,
`
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
report_key TEXT PRIMARY KEY,
mode TEXT NOT NULL,
group_levels_key TEXT NOT NULL,
main_group TEXT NOT NULL DEFAULT '',
row_count INTEGER NOT NULL DEFAULT 0,
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
duration_ms BIGINT NOT NULL DEFAULT 0
)`,
}
for _, stmt := range stmts {
if _, err := pg.Exec(stmt); err != nil {
@@ -570,6 +649,523 @@ WHERE btrim(askili_yan) = '-'`,
return nil
}
func productPerformanceSnapshotBypass(ctx context.Context) context.Context {
return context.WithValue(ctx, productPerformanceSnapshotBypassKey{}, true)
}
func productPerformanceUseSnapshot(ctx context.Context) bool {
v, _ := ctx.Value(productPerformanceSnapshotBypassKey{}).(bool)
return !v
}
func productPerformanceLiveFallbackEnabled() bool {
raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_PERFORMANCE_LIVE_FALLBACK")))
return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}
func productPerformanceSnapshotKey(parts ...string) string {
cleaned := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.ToLower(strings.TrimSpace(part))
if part != "" {
cleaned = append(cleaned, part)
}
}
return strings.Join(cleaned, ":")
}
func loadProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]T, bool, error) {
if !productPerformanceUseSnapshot(ctx) || pg == nil || strings.TrimSpace(reportKey) == "" {
return nil, false, nil
}
if limit <= 0 || limit > 50000 {
limit = 50000
}
rows, err := pg.QueryContext(ctx, `
SELECT payload
FROM mk_product_performance_report_snapshot
WHERE report_key = $1
ORDER BY row_order
LIMIT $2
`, reportKey, limit)
if err != nil {
return nil, false, err
}
defer rows.Close()
out := make([]T, 0, limit)
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, false, err
}
var item T
if err := json.Unmarshal(raw, &item); err != nil {
return nil, false, err
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, false, err
}
if len(out) > 0 {
return out, true, nil
}
var exists bool
if err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1
FROM mk_product_performance_report_snapshot_meta
WHERE report_key = $1
)
`, reportKey).Scan(&exists); err != nil {
return nil, false, err
}
if !exists && !productPerformanceLiveFallbackEnabled() {
return out, true, nil
}
return out, exists, nil
}
func loadProductPerformanceSnapshotMapRows(ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]map[string]any, bool, error) {
if !productPerformanceUseSnapshot(ctx) || pg == nil || strings.TrimSpace(reportKey) == "" {
return nil, false, nil
}
if limit <= 0 || limit > 50000 {
limit = 50000
}
rows, err := pg.QueryContext(ctx, `
SELECT payload
FROM mk_product_performance_report_snapshot
WHERE report_key = $1
ORDER BY row_order
LIMIT $2
`, reportKey, limit)
if err != nil {
return nil, false, err
}
defer rows.Close()
out := make([]map[string]any, 0, limit)
for rows.Next() {
var raw []byte
if err := rows.Scan(&raw); err != nil {
return nil, false, err
}
var item map[string]any
if err := json.Unmarshal(raw, &item); err != nil {
return nil, false, err
}
if item == nil {
item = map[string]any{}
}
out = append(out, item)
}
if err := rows.Err(); err != nil {
return nil, false, err
}
if len(out) > 0 {
return out, true, nil
}
var exists bool
if err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
SELECT 1
FROM mk_product_performance_report_snapshot_meta
WHERE report_key = $1
)
`, reportKey).Scan(&exists); err != nil {
return nil, false, err
}
if !exists && !productPerformanceLiveFallbackEnabled() {
return out, true, nil
}
return out, exists, nil
}
func loadProductPerformanceSnapshotItem[T any](ctx context.Context, pg *sql.DB, reportKey string) (T, bool, error) {
var zero T
rows, ok, err := loadProductPerformanceSnapshotRows[T](ctx, pg, reportKey, 1)
if err != nil || !ok || len(rows) == 0 {
return zero, ok, err
}
return rows[0], true, nil
}
func saveProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, rows []T, started time.Time) error {
if pg == nil || strings.TrimSpace(reportKey) == "" {
return nil
}
tx, err := pg.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_report_snapshot WHERE report_key = $1`, reportKey); err != nil {
return err
}
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot (report_key, row_order, payload, updated_at)
VALUES ($1, $2, $3, now())
`)
if err != nil {
return err
}
defer stmt.Close()
for i, row := range rows {
raw, err := json.Marshal(row)
if err != nil {
return err
}
if _, err := stmt.ExecContext(ctx, reportKey, i, raw); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot_meta (report_key, row_count, refreshed_at, duration_ms)
VALUES ($1, $2, now(), $3)
ON CONFLICT (report_key) DO UPDATE SET
row_count = EXCLUDED.row_count,
refreshed_at = EXCLUDED.refreshed_at,
duration_ms = EXCLUDED.duration_ms
`, reportKey, len(rows), time.Since(started).Milliseconds()); err != nil {
return err
}
return tx.Commit()
}
func saveProductPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, reportKey string, def productPerformanceGroupedSnapshotDefinition, rows []map[string]any, started time.Time) error {
if pg == nil || strings.TrimSpace(reportKey) == "" {
return nil
}
tx, err := pg.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key = $1`, reportKey); err != nil {
return err
}
stmt, err := tx.PrepareContext(ctx, `
INSERT INTO mk_product_performance_grouped_snapshot (
report_key, row_order, mode, group_levels_key, main_group,
group_level, group_key, parent_key, group_field, group_value, payload, updated_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now())
`)
if err != nil {
return err
}
defer stmt.Close()
levelsKey := productPerformanceGroupedLevelsKey(def.Levels)
for i, row := range rows {
raw, err := json.Marshal(row)
if err != nil {
return err
}
key := stringFromMap(row, "key")
if _, err := stmt.ExecContext(
ctx,
reportKey,
i,
strings.TrimSpace(def.Mode),
levelsKey,
strings.TrimSpace(def.MainGroup),
intFromMap(row, "level"),
key,
productPerformanceParentGroupKey(key),
stringFromMap(row, "group_field"),
stringFromMap(row, "group_value"),
raw,
); err != nil {
return err
}
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_grouped_snapshot_meta (
report_key, mode, group_levels_key, main_group, row_count, refreshed_at, duration_ms
)
VALUES ($1,$2,$3,$4,$5,now(),$6)
ON CONFLICT (report_key) DO UPDATE SET
mode = EXCLUDED.mode,
group_levels_key = EXCLUDED.group_levels_key,
main_group = EXCLUDED.main_group,
row_count = EXCLUDED.row_count,
refreshed_at = EXCLUDED.refreshed_at,
duration_ms = EXCLUDED.duration_ms
`, reportKey, strings.TrimSpace(def.Mode), levelsKey, strings.TrimSpace(def.MainGroup), len(rows), time.Since(started).Milliseconds()); err != nil {
return err
}
return tx.Commit()
}
func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
if err := EnsureProductPerformanceTables(pg); err != nil {
return 0, err
}
snapshotReadCtx := ctx
ctx = productPerformanceSnapshotBypass(ctx)
total := 0
save := func(reportKey string, rows any, started time.Time) error {
rowCount := productPerformanceSnapshotPayloadLen(rows)
log.Printf("[ProductPerformanceRefresh] snapshot save start key=%s rows=%d", reportKey, rowCount)
defer func() {
log.Printf("[ProductPerformanceRefresh] snapshot save done key=%s rows=%d elapsed=%s", reportKey, rowCount, time.Since(started).Round(time.Second))
}()
switch v := rows.(type) {
case []models.ProductPerformanceSummary:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceGeneralRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderAnalysisRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderGroupRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderProductCustomerRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceOrderMarketDetailRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceMarketRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceCountryRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceCustomerRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
case []models.ProductPerformanceSalesBreakdownRow:
total += len(v)
return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
default:
return fmt.Errorf("unsupported product performance snapshot payload %s", reportKey)
}
}
started := time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("summary"))
if summary, err := GetProductPerformanceSummary(ctx, pg); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("summary"), []models.ProductPerformanceSummary{summary}, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("products"))
if rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: 50000, Page: 1, SortBy: "performance_score", Descending: true}); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("products"), rows, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("general"))
if rows, err := ListProductPerformanceGeneral(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("general"), rows, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("orders"))
if rows, err := ListProductPerformanceOrderAnalysis(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("orders"), rows, started); err != nil {
return total, err
}
for _, mode := range []string{"market", "customer"} {
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-groups", mode))
rows, err := ListProductPerformanceOrderGroups(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("order-groups", mode), rows, started); err != nil {
return total, err
}
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-product-customers"))
if rows, err := ListProductPerformanceOrderProductCustomers(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("order-product-customers"), rows, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-market-details"))
if rows, err := ListProductPerformanceOrderMarketDetails(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("order-market-details"), rows, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("markets"))
if rows, err := ListProductPerformanceMarkets(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("markets"), rows, started); err != nil {
return total, err
}
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("countries"))
if rows, err := ListProductPerformanceCountries(ctx, pg, 50000); err != nil {
return total, err
} else if err := save(productPerformanceSnapshotKey("countries"), rows, started); err != nil {
return total, err
}
for _, mode := range []string{"market_customer", "country_customer", "market_country_customer"} {
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("customers", mode))
rows, err := ListProductPerformanceCustomers(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("customers", mode), rows, started); err != nil {
return total, err
}
}
for _, mode := range []string{"color_yaka_market_customer", "product_country_segment_market_customer", "market_customer_product", "country_segment_market_customer_product"} {
started = time.Now()
log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("sales-breakdown", mode))
rows, err := ListProductPerformanceSalesBreakdown(ctx, pg, mode, 50000)
if err != nil {
return total, err
}
if err := save(productPerformanceSnapshotKey("sales-breakdown", mode), rows, started); err != nil {
return total, err
}
}
log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild start")
groupedRows, err := RebuildProductPerformanceGroupedSnapshots(snapshotReadCtx, pg)
if err != nil {
return total, err
}
log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild done rows=%d", groupedRows)
total += groupedRows
return total, nil
}
func productPerformanceSnapshotPayloadLen(rows any) int {
switch v := rows.(type) {
case []models.ProductPerformanceSummary:
return len(v)
case []models.ProductPerformanceRow:
return len(v)
case []models.ProductPerformanceGeneralRow:
return len(v)
case []models.ProductPerformanceOrderAnalysisRow:
return len(v)
case []models.ProductPerformanceOrderGroupRow:
return len(v)
case []models.ProductPerformanceOrderProductCustomerRow:
return len(v)
case []models.ProductPerformanceOrderMarketDetailRow:
return len(v)
case []models.ProductPerformanceMarketRow:
return len(v)
case []models.ProductPerformanceCountryRow:
return len(v)
case []models.ProductPerformanceCustomerRow:
return len(v)
case []models.ProductPerformanceSalesBreakdownRow:
return len(v)
default:
return 0
}
}
type productPerformanceGroupedSnapshotDefinition struct {
Mode string
Levels []string
MainGroup string
}
func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
if err := EnsureProductPerformanceTables(pg); err != nil {
return 0, err
}
defs, err := productPerformanceGroupedSnapshotDefinitions(ctx, pg)
if err != nil {
return 0, err
}
log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
if err := deleteStaleProductPerformanceGroupedSnapshots(ctx, pg, productPerformanceSnapshotKey("grouped", "product_detail")+":%"); err != nil {
return 0, err
}
total := 0
for i, def := range defs {
started := time.Now()
levels := sanitizeProductPerformanceGroupLevels(def.Levels)
if len(levels) == 0 {
levels = defaultProductPerformanceGroupLevels(def.Mode)
}
reportKey := productPerformanceGroupedSnapshotReportKey(def.Mode, levels, def.MainGroup)
log.Printf("[ProductPerformanceRefresh] grouped snapshot start %d/%d key=%s mode=%s main_group=%s levels=%s", i+1, len(defs), reportKey, def.Mode, def.MainGroup, productPerformanceGroupedLevelsKey(levels))
sourceRows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, def.Mode, 50000)
if err != nil {
return total, err
}
log.Printf("[ProductPerformanceRefresh] grouped snapshot source loaded key=%s source_rows=%d elapsed=%s", reportKey, len(sourceRows), time.Since(started).Round(time.Second))
req := ProductPerformanceGroupedRequest{
Mode: def.Mode,
MainGroup: def.MainGroup,
}
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
groupStarted := time.Now()
rows := buildProductPerformanceGroupedSnapshotRows(sourceRows, levels, def.Mode)
log.Printf("[ProductPerformanceRefresh] grouped snapshot tree built key=%s rows=%d elapsed=%s total_elapsed=%s", reportKey, len(rows), time.Since(groupStarted).Round(time.Second), time.Since(started).Round(time.Second))
if err := saveProductPerformanceGroupedSnapshotRows(ctx, pg, reportKey, productPerformanceGroupedSnapshotDefinition{
Mode: def.Mode,
Levels: levels,
MainGroup: def.MainGroup,
}, rows, started); err != nil {
return total, err
}
total += len(rows)
log.Printf("[ProductPerformanceRefresh] grouped snapshot done key=%s rows=%d cumulative_rows=%d elapsed=%s", reportKey, len(rows), total, time.Since(started).Round(time.Second))
}
return total, nil
}
func deleteStaleProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB, reportKeyLike string) error {
reportKeyLike = strings.TrimSpace(reportKeyLike)
if reportKeyLike == "" {
return nil
}
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key LIKE $1`, reportKeyLike); err != nil {
return err
}
if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot_meta WHERE report_key LIKE $1`, reportKeyLike); err != nil {
return err
}
return nil
}
func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
_ = ctx
_ = pg
defs := []productPerformanceGroupedSnapshotDefinition{
{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
{Mode: "products", Levels: []string{"urun_ana_grubu"}},
{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
{Mode: "sales_color_yaka_market_customer", Levels: []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}},
{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
}
return defs, nil
}
func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
started := time.Now()
stage := productPerformanceRefreshStage(req.Stage)
@@ -689,6 +1285,7 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
}
var kpiRows int
var snapshotRows int
if shouldRun("kpi") {
if err := runStage("kpi", func(tx *sql.Tx) error {
log.Printf("[ProductPerformanceRefresh] kpi rebuild start")
@@ -702,20 +1299,28 @@ func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerfo
}); err != nil {
return ProductPerformanceRefreshResult{}, err
}
log.Printf("[ProductPerformanceRefresh] report snapshots rebuild start")
rows, err := RebuildProductPerformanceReportSnapshots(ctx, pg)
if err != nil {
return ProductPerformanceRefreshResult{}, err
}
snapshotRows = rows
log.Printf("[ProductPerformanceRefresh] report snapshots rebuild done rows=%d elapsed=%s", snapshotRows, time.Since(started).Round(time.Second))
} else {
log.Printf("[ProductPerformanceRefresh] kpi skipped stage=%s", stage)
}
log.Printf("[ProductPerformanceRefresh] refresh done total_elapsed=%s", time.Since(started).Round(time.Second))
return ProductPerformanceRefreshResult{
Mode: mode,
Stage: stage,
StartDate: req.StartDate.Format("2006-01-02"),
EndDate: req.EndDate.Format("2006-01-02"),
SalesRows: salesRows,
StockRows: stockRows,
KpiRows: kpiRows,
DurationMS: time.Since(started).Milliseconds(),
Mode: mode,
Stage: stage,
StartDate: req.StartDate.Format("2006-01-02"),
EndDate: req.EndDate.Format("2006-01-02"),
SalesRows: salesRows,
StockRows: stockRows,
KpiRows: kpiRows,
SnapshotRows: snapshotRows,
DurationMS: time.Since(started).Milliseconds(),
}, nil
}
@@ -1168,6 +1773,14 @@ func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanc
if page <= 0 {
page = 1
}
if !productPerformanceHasServerFilters(f) {
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
return nil, 0, err
} else if ok {
applyProductPerformanceProductRowScores(rows)
return rows, len(rows), nil
}
}
where, args := productPerformanceWhere(f)
countQuery := `SELECT COUNT(*) FROM mk_product_performance_kpi_daily WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + where
var total int
@@ -1183,11 +1796,15 @@ SELECT
sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total,
avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total,
avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total,
stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total,
stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total,
avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d, market_count_90d, customer_count_90d, sales_index_90d,
unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
market_count_90d, market_count_180d, market_count_365d, market_count_total,
customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total,
price_index_90d, margin_index_90d, performance_score, performance_bucket,
recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number,
to_char(updated_at,'YYYY-MM-DD HH24:MI:SS')
@@ -1209,11 +1826,15 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730, &r.SalesQtyTotal,
&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.SalesUSDTotal,
&r.AvgDailySales90, &r.AvgDailySales180, &r.AvgDailySales365, &r.AvgDailySalesTotal,
&r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal,
&r.StockDays90, &r.StockDays180, &r.StockDays365, &r.StockDaysTotal,
&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal,
&r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
&r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180,
&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180, &r.MarketCount90, &r.CustomerCount90, &r.SalesIndex90,
&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180,
&r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal,
&r.CustomerCount90, &r.CustomerCount180, &r.CustomerCount365, &r.CustomerCountTotal,
&r.SalesIndex90, &r.SalesIndex180, &r.SalesIndex365, &r.SalesIndexTotal,
&r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
); err != nil {
@@ -1230,13 +1851,125 @@ LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
for i := range out {
out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
}
applyProductPerformanceProductRowScores(out)
return out, total, nil
}
func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
avg := productPerformanceProductRowAverages(rows)
for i := range rows {
rows[i].SalesIndex90 = productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90)
rows[i].SalesIndex180 = productPerformanceRelativeIndex(rows[i].SalesUSD180, avg.salesUSD180)
rows[i].SalesIndex365 = productPerformanceRelativeIndex(rows[i].SalesUSD365, avg.salesUSD365)
rows[i].SalesIndexTotal = productPerformanceRelativeIndex(rows[i].SalesUSDTotal, avg.salesUSDTotal)
margin365 := productPerformanceMarginFromSales(rows[i].SalesUSD365, rows[i].SalesQty365, rows[i].CostPriceUSD)
marginTotal := productPerformanceMarginFromSales(rows[i].SalesUSDTotal, rows[i].SalesQtyTotal, rows[i].CostPriceUSD)
rows[i].PerformanceScore90 = productPerformanceProductScore(
"90d",
rows[i].SalesUSD90,
rows[i].SalesIndex90,
rows[i].GrossMargin90,
rows[i].StockTurnover90,
float64(rows[i].MarketCount90),
float64(rows[i].CustomerCount90),
)
rows[i].PerformanceScore180 = productPerformanceProductScore(
"180d",
rows[i].SalesUSD180,
rows[i].SalesIndex180,
rows[i].GrossMargin180,
rows[i].StockTurnover180,
float64(rows[i].MarketCount180),
float64(rows[i].CustomerCount180),
)
rows[i].PerformanceScore365 = productPerformanceProductScore(
"365d",
rows[i].SalesUSD365,
rows[i].SalesIndex365,
margin365,
rows[i].StockTurnover365,
float64(rows[i].MarketCount365),
float64(rows[i].CustomerCount365),
)
rows[i].PerformanceScoreTotal = productPerformanceProductScore(
"total",
rows[i].SalesUSDTotal,
rows[i].SalesIndexTotal,
marginTotal,
rows[i].StockTurnoverTotal,
float64(rows[i].MarketCountTotal),
float64(rows[i].CustomerCountTotal),
productPerformanceProductRowTotalPeriodDays(rows[i]),
)
rows[i].PerformanceScore = rows[i].PerformanceScore90
}
}
type productPerformanceProductRowAverage struct {
salesUSD90 float64
salesUSD180 float64
salesUSD365 float64
salesUSDTotal float64
}
func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
var sum90, count90, sum180, count180, sum365, count365, sumTotal, countTotal float64
for _, row := range rows {
if row.SalesUSD90 > 0 {
sum90 += row.SalesUSD90
count90++
}
if row.SalesUSD180 > 0 {
sum180 += row.SalesUSD180
count180++
}
if row.SalesUSD365 > 0 {
sum365 += row.SalesUSD365
count365++
}
if row.SalesUSDTotal > 0 {
sumTotal += row.SalesUSDTotal
countTotal++
}
}
out := productPerformanceProductRowAverage{}
if count90 > 0 {
out.salesUSD90 = sum90 / count90
}
if count180 > 0 {
out.salesUSD180 = sum180 / count180
}
if count365 > 0 {
out.salesUSD365 = sum365 / count365
}
if countTotal > 0 {
out.salesUSDTotal = sumTotal / countTotal
}
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
}
return (salesUSD - (salesQty * unitCost)) / salesUSD
}
func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
if err := EnsureProductPerformanceTables(pg); err != nil {
return models.ProductPerformanceSummary{}, err
}
if summary, ok, err := loadProductPerformanceSnapshotItem[models.ProductPerformanceSummary](ctx, pg, productPerformanceSnapshotKey("summary")); err != nil {
return models.ProductPerformanceSummary{}, err
} else if ok {
return summary, nil
}
var s models.ProductPerformanceSummary
err := pg.QueryRowContext(ctx, `
WITH Latest AS (
@@ -1247,7 +1980,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')
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')
),
VariantStock AS (
SELECT
@@ -1274,7 +2007,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')
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')
)
SELECT
COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
@@ -1307,6 +2040,12 @@ func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) (
} else if limit > 50000 {
limit = 50000
}
if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
return nil, err
} else if ok {
applyProductPerformanceGeneralRowScores(rows)
return rows, nil
}
rows, err := pg.QueryContext(ctx, `
WITH Bounds AS (
SELECT
@@ -1340,11 +2079,11 @@ SalesAgg AS (
MAX(last_ref_number) AS last_ref_number,
COALESCE(SUM(sales_qty),0) AS sales_qty_total,
COALESCE(SUM(sales_usd),0) AS sales_usd_total,
COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')),0)::integer AS customer_count_total,
COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0),0)::integer AS customer_count_total,
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')
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')
GROUP BY product_code, color_code, yaka_kodu, market_key
),
StockAgg AS (
@@ -1396,7 +2135,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')
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')
UNION ALL
SELECT
product_code,
@@ -1412,7 +2151,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')
WHERE 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')
GROUP BY product_code, color_code, yaka_kodu
) x
ORDER BY product_code, color_code, yaka_kodu, src_rank
@@ -1444,7 +2183,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 upper(translate(btrim(COALESCE(d.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', '