Fix product performance grouped JSON build
This commit is contained in:
+326
-140
@@ -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") ||
|
||||
|
||||
Reference in New Issue
Block a user