Compare commits
74
Commits
4ae6475875
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db13574e9d | ||
|
|
ba9a744aef | ||
|
|
82ad3783b3 | ||
|
|
322bed753f | ||
|
|
bdc9459330 | ||
|
|
2d85a33623 | ||
|
|
5f1c37859b | ||
|
|
31a71ab566 | ||
|
|
25ef023df5 | ||
|
|
0c51729f7a | ||
|
|
20f5fea2e8 | ||
|
|
65e8aa2b62 | ||
|
|
1f8355b77c | ||
|
|
11048f1555 | ||
|
|
b83a3d22ce | ||
|
|
77af9ef533 | ||
|
|
4395d6bae2 | ||
|
|
6bc19e7a43 | ||
|
|
797d069e59 | ||
|
|
36e0ec9ff5 | ||
|
|
f14a96a024 | ||
|
|
f877b611b7 | ||
|
|
dbb3f3f380 | ||
|
|
03287d4850 | ||
|
|
12ce3a3eca | ||
|
|
52420bc5ff | ||
|
|
d5aa43ccb5 | ||
|
|
04461acb58 | ||
|
|
cd101811c4 | ||
|
|
c4028010c0 | ||
|
|
c2d010751f | ||
|
|
a9ab9fd9b0 | ||
|
|
1945fd4456 | ||
|
|
7b8f8905e5 | ||
|
|
89df3d2d3d | ||
|
|
474904481f | ||
|
|
6eaf0e3ed3 | ||
|
|
8249761415 | ||
|
|
dbd84f7399 | ||
|
|
9edc806345 | ||
|
|
7a7008cc1b | ||
|
|
3e0253f6be | ||
|
|
b80b5024f4 | ||
|
|
5eaf8be9e6 | ||
|
|
cc2930caf2 | ||
|
|
26b4a63c45 | ||
|
|
c6a3d1552e | ||
|
|
005d2eafac | ||
|
|
289f204752 | ||
|
|
1448dab9d7 | ||
|
|
5d4216d42e | ||
|
|
3993c363cb | ||
|
|
a0da3f2bed | ||
|
|
19a42e0551 | ||
|
|
53cdd1a4a1 | ||
|
|
fe29e763c5 | ||
|
|
9e1ecb9b3f | ||
|
|
fbdde54eee | ||
|
|
b2113a8d49 | ||
|
|
28e33d5e58 | ||
|
|
fbdc4934ff | ||
|
|
5b512fd30a | ||
|
|
7879172c54 | ||
|
|
c602e1e119 | ||
|
|
27934bccdf | ||
|
|
48bde09c2c | ||
|
|
480cad0d6f | ||
|
|
9f6e288d04 | ||
|
|
d0717900a6 | ||
|
|
25bc9b2d02 | ||
|
|
4c56062ef4 | ||
|
|
60b2f76537 | ||
|
|
81384426ff | ||
|
|
3697e35064 |
Generated
+1
@@ -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" />
|
||||
|
||||
@@ -3,6 +3,8 @@ bssapp
|
||||
|
||||
# Env
|
||||
.env
|
||||
.env.local
|
||||
.env.argebaggi.local
|
||||
mail.env
|
||||
|
||||
# Runtime fonts
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
@@ -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', ' | ||||