package queries import ( "bssapp-backend/db" "context" "database/sql" "fmt" "strings" ) // GetProductPricingFilterOptions returns distinct option values for ProductPricing filters. // This is used to render filter dropdowns without loading the full dataset. func GetProductPricingFilterOptions(ctx context.Context, field string, q string, limit int, scopeUrunIlkGrubu []string) ([]string, error) { pg := db.PgDB mssql := db.MssqlDB if mssql == nil { // Some option fields can still be served from PG cache table. if pg == nil { return nil, fmt.Errorf("mssql db is nil") } } field = strings.TrimSpace(field) q = strings.TrimSpace(q) if limit <= 0 || limit > 200 { limit = 120 } if len(scopeUrunIlkGrubu) > 3 { scopeUrunIlkGrubu = scopeUrunIlkGrubu[:3] } // Fast path: use PG-derived pricing parameter cache for most fields. // This avoids scanning ProductFilterWithDescription('TR') on MSSQL, which can be slow and cause 504s. // productCode is not available in mk_urunpricingprmtr, keep MSSQL for that. if pg != nil && field != "productCode" { pgCol := "" switch field { case "brandGroupSelection": pgCol = "brand_group_sec" case "marka": pgCol = "marka" case "askiliYan": pgCol = "askili_yan" case "kategori": pgCol = "kategori" case "urunIlkGrubu": pgCol = "urun_ilk_grubu" case "urunAnaGrubu": pgCol = "urun_ana_grubu" case "urunAltGrubu": pgCol = "urun_alt_grubu" case "icerik": pgCol = "icerik" case "karisim": // "karisim" is intentionally deprecated in mk_urunpricingprmtr, keep MSSQL fallback. pgCol = "" default: pgCol = "" } if pgCol != "" { args := make([]any, 0, 8) where := []string{ "is_active = TRUE", fmt.Sprintf("NULLIF(BTRIM(%s), '') IS NOT NULL", pgCol), } if len(scopeUrunIlkGrubu) > 0 && field != "urunIlkGrubu" { args = append(args, scopeUrunIlkGrubu) where = append(where, fmt.Sprintf("urun_ilk_grubu = ANY($%d::text[])", len(args))) } if q != "" { like := q + "%" args = append(args, like) where = append(where, fmt.Sprintf("%s ILIKE $%d", pgCol, len(args))) } whereSQL := strings.Join(where, " AND ") // Note: DISTINCT+ORDER+LIMIT is fine here due to small mk_urunpricingprmtr cardinality (~1000 rows). sqlText := fmt.Sprintf(` SELECT DISTINCT %s AS val FROM mk_urunpricingprmtr WHERE %s ORDER BY val ASC LIMIT %d `, pgCol, whereSQL, limit) rows, err := pg.QueryContext(ctx, sqlText, args...) if err == nil { defer rows.Close() out := make([]string, 0, limit) for rows.Next() { var v sql.NullString if err := rows.Scan(&v); err != nil { return nil, err } if s := strings.TrimSpace(v.String); s != "" { out = append(out, s) } } if err := rows.Err(); err != nil { return nil, err } return out, nil } // If PG path fails, fall back to MSSQL below. } } // Map UI filter fields -> MSSQL expression in ProductFilterWithDescription('TR') var expr string switch field { case "productCode": expr = "LTRIM(RTRIM(ProductCode))" case "brandGroupSelection": expr = `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3 WHEN 0 THEN 'MARKA GRUBU A' WHEN 1 THEN 'MARKA GRUBU B' ELSE 'MARKA GRUBU C' END` case "marka": expr = "COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')" case "askiliYan": expr = "COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')" case "kategori": expr = "COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')" case "urunIlkGrubu": expr = "COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')" case "urunAnaGrubu": expr = "COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')" case "urunAltGrubu": expr = "COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')" case "icerik": expr = "COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')" case "karisim": expr = "COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')" default: return nil, fmt.Errorf("invalid field") } // NOTE: We keep the same base constraints as the listing query. // q: prefix match to keep it sargable-ish. args := make([]any, 0, 8) where := []string{ "ProductAtt42 IN ('SERI', 'AKSESUAR')", "IsBlocked = 0", "LEN(LTRIM(RTRIM(ProductCode))) = 13", } if len(scopeUrunIlkGrubu) > 0 && field != "urunIlkGrubu" { // Cascade scope: allow limiting options by the already selected "Urun Ilk Grubu" (desc). // We filter by desc value because UI uses desc fields. placeholders := make([]string, 0, len(scopeUrunIlkGrubu)) for _, v := range scopeUrunIlkGrubu { v = strings.TrimSpace(v) if v == "" { continue } placeholders = append(placeholders, fmt.Sprintf("@p%d", len(args)+1)) args = append(args, v) } if len(placeholders) > 0 { where = append(where, fmt.Sprintf("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') IN (%s)", strings.Join(placeholders, ", "))) } } if q != "" { // For productCode, allow contains if user types middle; for others use prefix. if field == "productCode" { where = append(where, expr+fmt.Sprintf(" LIKE @p%d", len(args)+1)) args = append(args, "%"+q+"%") } else { where = append(where, expr+fmt.Sprintf(" LIKE @p%d", len(args)+1)) args = append(args, q+"%") } } whereSQL := strings.Join(where, " AND ") sqlText := fmt.Sprintf(` SELECT TOP (%d) X.val FROM ( SELECT DISTINCT NULLIF(%s, '') AS val FROM ProductFilterWithDescription('TR') WHERE %s ) X WHERE X.val IS NOT NULL ORDER BY X.val ASC; `, limit, expr, whereSQL) rows, err := mssql.QueryContext(ctx, sqlText, args...) if err != nil { return nil, err } defer rows.Close() out := make([]string, 0, limit) for rows.Next() { var v sql.NullString if err := rows.Scan(&v); err != nil { return nil, err } if s := strings.TrimSpace(v.String); s != "" { out = append(out, s) } } return out, rows.Err() }