Fix product performance grouped JSON build

This commit is contained in:
M_Kececi
2026-07-06 00:53:30 +03:00
parent 19a42e0551
commit a0da3f2bed
2 changed files with 513 additions and 203 deletions
+326 -140
View File
@@ -1072,6 +1072,9 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
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()
@@ -1107,7 +1110,23 @@ func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB)
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"}},
@@ -1118,30 +1137,6 @@ func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.D
{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"}},
}
productRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("products"), 50000)
if err != nil {
return nil, err
}
if ok {
mainGroups := make([]string, 0)
seen := map[string]bool{}
for _, row := range productRows {
mainGroup := productPerformanceGroupedFilterValue(row, "urun_ana_grubu")
if mainGroup == "" || seen[mainGroup] {
continue
}
seen[mainGroup] = true
mainGroups = append(mainGroups, mainGroup)
}
sort.Strings(mainGroups)
for _, mainGroup := range mainGroups {
defs = append(defs, productPerformanceGroupedSnapshotDefinition{
Mode: "product_detail",
Levels: []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"},
MainGroup: mainGroup,
})
}
}
return defs, nil
}
@@ -3920,6 +3915,7 @@ func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductP
sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
out := make([]map[string]any, 0, len(sourceRows))
appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
return out, nil
}
@@ -4038,6 +4034,9 @@ func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB,
}
effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
hasFilters := len(effectiveFilters) > 0
if hasFilters {
return nil, false, nil
}
hasManualExpansion := len(req.ExpandedKeys) > 0
query := `
SELECT payload
@@ -4098,7 +4097,6 @@ SELECT EXISTS (
return nil, false, nil
}
}
out = filterProductPerformancePreparedGroupedRows(out, effectiveFilters)
out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
return out, true, nil
@@ -4988,9 +4986,9 @@ func productPerformanceGroupedFilterValue(row map[string]any, field string) stri
}
func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
if rows, ok, err := productPerformanceGroupedSnapshotRows(ctx, pg, mode, limit); err != nil {
if rows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, mode, limit); err != nil {
return nil, err
} else if ok {
} else if rows != nil {
return rows, nil
}
@@ -5036,6 +5034,7 @@ func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql
}
if mode == "products" || mode == "product_detail" {
rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows)
}
return rows, nil
}
@@ -5076,6 +5075,88 @@ func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.
return rows
}
func mergeProductPerformanceSalesSpreadKeys(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
if len(rows) == 0 {
return rows
}
const query = `
WITH Latest AS (
SELECT COALESCE(
(SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
(SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
current_date
)::date AS kpi_date
),
Spread AS (
SELECT
s.product_code,
s.color_code,
s.yaka_kodu,
s.market_key,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_90d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_180d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_365d,
COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND COALESCE(s.sales_usd,0) > 0
AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
), '[]'::jsonb) AS customer_keys_total
FROM mk_product_performance_sales_daily s
CROSS JOIN Latest
WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
AND upper(translate(btrim(COALESCE(s.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 s.product_code, s.color_code, s.yaka_kodu, s.market_key
)
SELECT jsonb_build_object(
'product_code', product_code,
'color_code', color_code,
'yaka_kodu', yaka_kodu,
'market_key', market_key,
'__customer_keys_90d', customer_keys_90d,
'__customer_keys_180d', customer_keys_180d,
'__customer_keys_365d', customer_keys_365d,
'__customer_keys_total', customer_keys_total
)
FROM Spread`
spreadRows, err := queryProductPerformanceJSONRows(ctx, pg, query)
if err != nil {
log.Printf("[ProductPerformanceRefresh] product sales spread keys skipped err=%v", err)
return rows
}
byKey := make(map[string]map[string]any, len(spreadRows))
for _, row := range spreadRows {
key := productPerformanceMapMarketVariantKey(row)
if key != "" {
byKey[key] = row
}
}
for _, row := range rows {
spread := byKey[productPerformanceMapMarketVariantKey(row)]
if spread == nil {
continue
}
for _, suffix := range productPerformancePeriodSuffixes() {
field := "__customer_keys_" + suffix
if value, ok := spread[field]; ok {
row[field] = value
}
}
}
return rows
}
func productPerformanceMapMarketVariantKey(row map[string]any) string {
productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
if productCode == "" {
@@ -5181,23 +5262,21 @@ type productPerformanceGroupedSnapshotAvgState struct {
}
type productPerformanceGroupedSnapshotNode struct {
Key string
Level int
Field string
Value string
Row map[string]any
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
Image map[string]any
Market90Seen map[string]bool
MarketTotalSeen map[string]bool
Customer90Seen map[string]bool
CustomerTotalSeen map[string]bool
Key string
Level int
Field string
Value string
Row map[string]any
Count int
Children map[string]*productPerformanceGroupedSnapshotNode
ChildOrder []string
StockMetricSeen map[string]map[string]bool
IdleSeen map[string]bool
Avg map[string]*productPerformanceGroupedSnapshotAvgState
BucketCounts map[string]int
Image map[string]any
MarketSeen map[string]map[string]bool
CustomerSeen map[string]map[string]bool
}
func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
@@ -5253,7 +5332,7 @@ func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
n.BucketCounts[bucket]++
}
for key, value := range row {
if key == "row_key" || key == "key" || isProductPerformanceMarginField(key) {
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) || isProductPerformanceMarginField(key) {
continue
}
switch {
@@ -5324,34 +5403,35 @@ func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map
func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
if floatFromMap(row, "sales_usd_90d") > 0 {
if n.Market90Seen == nil {
n.Market90Seen = map[string]bool{}
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if n.MarketSeen == nil {
n.MarketSeen = map[string]map[string]bool{}
}
if n.MarketSeen[suffix] == nil {
n.MarketSeen[suffix] = map[string]bool{}
}
n.MarketSeen[suffix][market] = true
}
n.Market90Seen[market] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if n.MarketTotalSeen == nil {
n.MarketTotalSeen = map[string]bool{}
}
n.MarketTotalSeen[market] = true
}
}
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" {
if floatFromMap(row, "sales_usd_90d") > 0 {
if n.Customer90Seen == nil {
n.Customer90Seen = map[string]bool{}
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if n.CustomerSeen == nil {
n.CustomerSeen = map[string]map[string]bool{}
}
if n.CustomerSeen[suffix] == nil {
n.CustomerSeen[suffix] = map[string]bool{}
}
n.CustomerSeen[suffix][customer] = true
}
n.Customer90Seen[customer] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if n.CustomerTotalSeen == nil {
n.CustomerTotalSeen = map[string]bool{}
}
n.CustomerTotalSeen[customer] = true
}
}
for _, suffix := range productPerformancePeriodSuffixes() {
n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
}
}
func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
@@ -5387,7 +5467,7 @@ func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
row[key] = state.Sum / state.Count
}
}
applyProductPerformanceDistinctSpread(row, n.Market90Seen, n.MarketTotalSeen, n.Customer90Seen, n.CustomerTotalSeen)
applyProductPerformanceDistinctSpread(row, n.MarketSeen, n.CustomerSeen)
deriveProductPerformanceGroupMetrics(row, n.Field)
if bucket := n.dominantBucket(); bucket != "" {
row["performance_bucket"] = bucket
@@ -5508,14 +5588,12 @@ func clearProductPerformanceGroupDimensions(row map[string]any, groupField, grou
func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
out := map[string]any{}
market90Seen := map[string]bool{}
marketTotalSeen := map[string]bool{}
customer90Seen := map[string]bool{}
customerTotalSeen := map[string]bool{}
marketSeen := map[string]map[string]bool{}
customerSeen := map[string]map[string]bool{}
for _, row := range rows {
addProductPerformanceDistinctSpread(row, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
addProductPerformanceDistinctSpread(row, marketSeen, customerSeen)
for key, value := range row {
if key == "row_key" || key == "key" {
if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) {
continue
}
if isProductPerformanceMarginField(key) {
@@ -5541,6 +5619,9 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
for _, row := range rows {
for key := range row {
if isProductPerformanceInternalGroupField(key) {
continue
}
if isProductPerformanceMarginField(key) {
continue
}
@@ -5549,62 +5630,119 @@ func aggregateProductPerformanceRows(rows []map[string]any, groupField string) m
}
}
}
applyProductPerformanceDistinctSpread(out, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen)
applyProductPerformanceDistinctSpread(out, marketSeen, customerSeen)
deriveProductPerformanceGroupMetrics(out, groupField)
out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
return out
}
func addProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
if market != "" && market != "STOK" {
if floatFromMap(row, "sales_usd_90d") > 0 {
market90Seen[market] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
marketTotalSeen[market] = true
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if marketSeen[suffix] == nil {
marketSeen[suffix] = map[string]bool{}
}
marketSeen[suffix][market] = true
}
}
}
customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
if customer != "" && customer != "-" {
if floatFromMap(row, "sales_usd_90d") > 0 {
customer90Seen[customer] = true
for _, suffix := range productPerformancePeriodSuffixes() {
if floatFromMap(row, "sales_usd_"+suffix) > 0 {
if customerSeen[suffix] == nil {
customerSeen[suffix] = map[string]bool{}
}
customerSeen[suffix][customer] = true
}
}
if floatFromMap(row, "sales_usd_total") > 0 {
customerTotalSeen[customer] = true
}
for _, suffix := range productPerformancePeriodSuffixes() {
customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
}
}
func applyProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
for _, suffix := range productPerformancePeriodSuffixes() {
marketField := "market_count_" + suffix
customerField := "customer_count_" + suffix
if seen := marketSeen[suffix]; len(seen) > 0 {
row[marketField] = len(seen)
}
if seen := customerSeen[suffix]; len(seen) > 0 {
row[customerField] = len(seen)
}
if floatFromMap(row, "sales_usd_"+suffix) > 0 && (suffix == "90d" || suffix == "total") {
if intFromMap(row, marketField) == 0 {
row[marketField] = 1
}
if intFromMap(row, customerField) == 0 {
if suffix == "total" {
row[customerField] = maxInt(1, intFromMap(row, "customer_count_90d"))
} else {
row[customerField] = 1
}
}
}
}
}
func applyProductPerformanceDistinctSpread(row map[string]any, market90Seen, marketTotalSeen, customer90Seen, customerTotalSeen map[string]bool) {
if len(market90Seen) > 0 {
row["market_count_90d"] = len(market90Seen)
func productPerformanceAddSeenStrings(seen map[string]map[string]bool, suffix string, values []string) map[string]map[string]bool {
if len(values) == 0 {
return seen
}
if len(marketTotalSeen) > 0 {
row["market_count_total"] = len(marketTotalSeen)
if seen == nil {
seen = map[string]map[string]bool{}
}
if len(customer90Seen) > 0 {
row["customer_count_90d"] = len(customer90Seen)
if seen[suffix] == nil {
seen[suffix] = map[string]bool{}
}
if len(customerTotalSeen) > 0 {
row["customer_count_total"] = len(customerTotalSeen)
}
if floatFromMap(row, "sales_usd_90d") > 0 {
if intFromMap(row, "market_count_90d") == 0 {
row["market_count_90d"] = 1
}
if intFromMap(row, "customer_count_90d") == 0 {
row["customer_count_90d"] = 1
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || value == "-" {
continue
}
seen[suffix][value] = true
}
if floatFromMap(row, "sales_usd_total") > 0 {
if intFromMap(row, "market_count_total") == 0 {
row["market_count_total"] = maxInt(1, intFromMap(row, "market_count_90d"))
}
if intFromMap(row, "customer_count_total") == 0 {
row["customer_count_total"] = maxInt(1, intFromMap(row, "customer_count_90d"))
}
return seen
}
func productPerformanceStringSliceFromMap(row map[string]any, field string) []string {
value, ok := row[field]
if !ok || value == nil {
return nil
}
switch v := value.(type) {
case []string:
return v
case []any:
out := make([]string, 0, len(v))
for _, item := range v {
text := strings.TrimSpace(fmt.Sprint(item))
if text != "" {
out = append(out, text)
}
}
return out
case string:
text := strings.TrimSpace(v)
if text == "" {
return nil
}
var parsed []string
if strings.HasPrefix(text, "[") && json.Unmarshal([]byte(text), &parsed) == nil {
return parsed
}
return []string{text}
default:
return nil
}
}
func isProductPerformanceInternalGroupField(field string) bool {
return strings.HasPrefix(field, "__")
}
func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
@@ -5711,7 +5849,7 @@ func productPerformanceMapVariantKey(row map[string]any) string {
func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
normalizeProductPerformanceCostFields(out)
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
for _, suffix := range productPerformancePeriodSuffixes() {
sales := floatFromMap(out, "sales_usd_"+suffix)
qty := floatFromMap(out, "sales_qty_"+suffix)
stockQty := floatFromMap(out, "stock_qty")
@@ -5719,6 +5857,19 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
if turnoverBase <= 0 {
turnoverBase = stockQty
}
days := productPerformancePeriodDays(out, suffix)
avgDaily := floatFromMap(out, "avg_daily_sales_"+suffix)
if days > 0 {
avgDaily = qty / days
out["avg_daily_sales_"+suffix] = avgDaily
}
if avgDaily > 0 && turnoverBase > 0 {
out["stock_days_"+suffix] = turnoverBase / avgDaily
} else if qty <= 0 && stockQty > 0 {
out["stock_days_"+suffix] = 9999
} else if _, ok := out["stock_days_"+suffix]; !ok {
out["stock_days_"+suffix] = 0
}
if turnoverBase > 0 {
out["stock_turnover_"+suffix] = qty / turnoverBase
} else {
@@ -5775,37 +5926,15 @@ func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string)
if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
}
if _, ok := out["customer_score_90d"]; !ok {
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
}
if _, ok := out["customer_score_180d"]; !ok {
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
}
if _, ok := out["customer_score_365d"]; !ok {
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
}
if _, ok := out["customer_score_total"]; !ok {
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
}
if _, ok := out["performance_score_90d"]; !ok {
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
}
if _, ok := out["performance_score_180d"]; !ok {
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
}
if _, ok := out["performance_score_365d"]; !ok {
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
}
if _, ok := out["performance_score_total"]; !ok {
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
}
if _, ok := out["performance_score"]; !ok {
if score, exists := productPerformanceOptionalFloat(out, "performance_score_90d"); exists {
out["performance_score"] = score
} else {
out["performance_score"] = productPerformanceGroupScore(out, groupField)
}
}
out["customer_score_90d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "90d"))
out["customer_score_180d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "180d"))
out["customer_score_365d"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "365d"))
out["customer_score_total"] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, "total"))
out["performance_score_90d"] = productPerformanceSalesPeriodScore(out, "90d")
out["performance_score_180d"] = productPerformanceSalesPeriodScore(out, "180d")
out["performance_score_365d"] = productPerformanceSalesPeriodScore(out, "365d")
out["performance_score_total"] = productPerformanceSalesPeriodScore(out, "total")
out["performance_score"] = out["performance_score_90d"]
if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
score := productPerformanceOrderGroupScore(out)
out["performance_score_90d"] = score
@@ -5832,11 +5961,56 @@ func normalizeProductPerformanceCostFields(row map[string]any) {
}
}
normalize("cost_price_usd", "base_price_usd")
for _, suffix := range []string{"90d", "180d", "365d", "total"} {
for _, suffix := range productPerformancePeriodSuffixes() {
normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
}
}
func productPerformancePeriodSuffixes() []string {
return []string{"90d", "180d", "365d", "total"}
}
func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
switch suffix {
case "90d":
return 90
case "180d":
return 180
case "365d":
return 365
case "total":
start := parseProductPerformanceDate(stringFromMap(row, "period_start"))
if start.IsZero() {
start = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
}
end := parseProductPerformanceDate(stringFromMap(row, "period_end"))
if end.IsZero() {
end = parseProductPerformanceDate(stringFromMap(row, "kpi_date"))
}
if end.IsZero() || end.Before(start) {
return 0
}
return end.Sub(start).Hours()/24 + 1
default:
return 0
}
}
func parseProductPerformanceDate(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" {
return time.Time{}
}
if len(value) >= len("2006-01-02") {
value = value[:len("2006-01-02")]
}
t, err := time.Parse("2006-01-02", value)
if err != nil {
return time.Time{}
}
return t
}
func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
value, ok := row[field]
if !ok {
@@ -6172,7 +6346,16 @@ func mapGroupValue(row map[string]any, field string) string {
}
func shouldSumProductPerformanceField(field string) bool {
return strings.HasSuffix(field, "_qty") ||
return strings.HasPrefix(field, "sales_qty_") ||
strings.HasPrefix(field, "sales_usd_") ||
strings.HasPrefix(field, "avg_daily_sales_") ||
strings.HasPrefix(field, "gross_profit_") ||
strings.HasPrefix(field, "invoice_count_") ||
strings.HasPrefix(field, "market_count_") ||
strings.HasPrefix(field, "customer_count_") ||
strings.HasPrefix(field, "product_group_count_") ||
strings.HasPrefix(field, "product_count_") ||
strings.HasSuffix(field, "_qty") ||
strings.HasSuffix(field, "_usd") ||
strings.HasSuffix(field, "_count") ||
strings.HasSuffix(field, "_value_usd") ||
@@ -6189,6 +6372,9 @@ func shouldSumProductPerformanceField(field string) bool {
}
func shouldAverageProductPerformanceField(field string) bool {
if strings.HasPrefix(field, "avg_daily_sales_") {
return false
}
return strings.HasPrefix(field, "avg_") ||
strings.HasPrefix(field, "unit_") ||
strings.HasPrefix(field, "base_price") ||