package queries import ( "bssapp-backend/db" "bssapp-backend/models" "context" "database/sql" "fmt" "math" "strconv" "strings" "time" ) type ProductPricingFilters struct { Search string ProductCode []string BrandGroup []string AskiliYan []string Kategori []string UrunIlkGrubu []string UrunAnaGrubu []string UrunAltGrubu []string Icerik []string Karisim []string Marka []string } type ProductPricingPage struct { Rows []models.ProductPricing TotalCount int TotalPages int Page int Limit int } func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters) (ProductPricingPage, error) { result := ProductPricingPage{ Rows: []models.ProductPricing{}, TotalCount: 0, TotalPages: 0, Page: page, Limit: limit, } if page <= 0 { page = 1 } if limit <= 0 { limit = 500 } offset := (page - 1) * limit paramIndex := 1 args := make([]any, 0, 64) nextParam := func() string { name := "@p" + strconv.Itoa(paramIndex) paramIndex++ return name } whereParts := []string{ "ProductAtt42 IN ('SERI', 'AKSESUAR')", "IsBlocked = 0", "LEN(LTRIM(RTRIM(ProductCode))) = 13", } addInFilter := func(expr string, values []string) { clean := make([]string, 0, len(values)) for _, v := range values { v = strings.TrimSpace(v) if v == "" { continue } clean = append(clean, v) } if len(clean) == 0 { return } ors := make([]string, 0, len(clean)) for _, v := range clean { p := nextParam() ors = append(ors, expr+" = "+p) args = append(args, v) } whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")") } brandGroupExpr := `CASE ABS(CHECKSUM(LTRIM(RTRIM(ProductCode)))) % 3 WHEN 0 THEN 'MARKA GRUBU A' WHEN 1 THEN 'MARKA GRUBU B' ELSE 'MARKA GRUBU C' END` addInFilter("LTRIM(RTRIM(ProductCode))", filters.ProductCode) addInFilter(brandGroupExpr, filters.BrandGroup) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')", filters.AskiliYan) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')", filters.Kategori) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')", filters.UrunIlkGrubu) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')", filters.UrunAnaGrubu) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')", filters.UrunAltGrubu) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')", filters.Icerik) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '')", filters.Karisim) addInFilter("COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')", filters.Marka) if q := strings.TrimSpace(filters.Search); q != "" { p := nextParam() args = append(args, "%"+q+"%") whereParts = append(whereParts, "("+strings.Join([]string{ "LTRIM(RTRIM(ProductCode)) LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') LIKE " + p, "COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') LIKE " + p, }, " OR ")+")") } whereSQL := strings.Join(whereParts, " AND ") countQuery := ` SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode))) FROM ProductFilterWithDescription('TR') WHERE ` + whereSQL + `; ` var totalCount int if err := db.MssqlDB.QueryRowContext(ctx, countQuery, args...).Scan(&totalCount); err != nil { return result, err } result.TotalCount = totalCount if totalCount == 0 { result.TotalPages = 0 result.Page = 1 return result, nil } totalPages := int(math.Ceil(float64(totalCount) / float64(limit))) if totalPages <= 0 { totalPages = 1 } if page > totalPages { page = totalPages offset = (page - 1) * limit } result.Page = page result.Limit = limit result.TotalPages = totalPages // Stage 1: fetch only paged products first (fast path). productQuery := ` IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes; IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base; SELECT f.ProductCode, MAX(f.BrandGroupSec) AS BrandGroupSec, MAX(f.AskiliYan) AS AskiliYan, MAX(f.Kategori) AS Kategori, MAX(f.UrunIlkGrubu) AS UrunIlkGrubu, MAX(f.UrunAnaGrubu) AS UrunAnaGrubu, MAX(f.UrunAltGrubu) AS UrunAltGrubu, MAX(f.Icerik) AS Icerik, MAX(f.Karisim) AS Karisim, MAX(f.Marka) AS Marka INTO #req_codes FROM ( SELECT LTRIM(RTRIM(ProductCode)) AS ProductCode, ` + brandGroupExpr + ` AS BrandGroupSec, COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '') AS AskiliYan, COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '') AS Kategori, COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '') AS UrunIlkGrubu, COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '') AS UrunAnaGrubu, COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '') AS UrunAltGrubu, COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '') AS Icerik, COALESCE(LTRIM(RTRIM(ProductAtt29Desc)), '') AS Karisim, COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '') AS Marka FROM ProductFilterWithDescription('TR') WHERE ` + whereSQL + ` ) f GROUP BY f.ProductCode; CREATE CLUSTERED INDEX IX_req_codes_ProductCode ON #req_codes(ProductCode); SELECT LTRIM(RTRIM(s.ItemCode)) AS ItemCode, SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 INTO #stock_base FROM trStock s WITH(NOLOCK) INNER JOIN #req_codes rc ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) WHERE s.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 GROUP BY LTRIM(RTRIM(s.ItemCode)); CREATE CLUSTERED INDEX IX_stock_base_ItemCode ON #stock_base(ItemCode); SELECT rc.ProductCode, rc.BrandGroupSec, rc.AskiliYan, rc.Kategori, rc.UrunIlkGrubu, rc.UrunAnaGrubu, rc.UrunAltGrubu, rc.Icerik, rc.Karisim, rc.Marka FROM #req_codes rc LEFT JOIN #stock_base sb ON sb.ItemCode = rc.ProductCode ORDER BY CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2)) DESC, rc.ProductCode ASC OFFSET ` + strconv.Itoa(offset) + ` ROWS FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY; ` var ( rows *sql.Rows rowsErr error ) for attempt := 1; attempt <= 3; attempt++ { var err error rows, err = db.MssqlDB.QueryContext(ctx, productQuery, args...) if err == nil { rowsErr = nil break } rowsErr = err if ctx.Err() != nil || !isTransientMSSQLNetworkError(err) || attempt == 3 { break } wait := time.Duration(attempt*300) * time.Millisecond select { case <-ctx.Done(): break case <-time.After(wait): } } if rowsErr != nil { return result, rowsErr } defer rows.Close() out := make([]models.ProductPricing, 0, limit) for rows.Next() { var item models.ProductPricing if err := rows.Scan( &item.ProductCode, &item.BrandGroupSec, &item.AskiliYan, &item.Kategori, &item.UrunIlkGrubu, &item.UrunAnaGrubu, &item.UrunAltGrubu, &item.Icerik, &item.Karisim, &item.Marka, ); err != nil { return result, err } out = append(out, item) } if err := rows.Err(); err != nil { return result, err } if len(out) == 0 { result.Rows = out return result, nil } // Stage 2: fetch metrics only for paged product codes. codes := make([]string, 0, len(out)) for _, item := range out { codes = append(codes, strings.TrimSpace(item.ProductCode)) } valueRows := make([]string, 0, len(codes)) metricArgs := make([]any, 0, len(codes)) for i, code := range codes { paramName := "@p" + strconv.Itoa(i+1) valueRows = append(valueRows, "("+paramName+")") metricArgs = append(metricArgs, code) } metricsQuery := ` WITH req_codes AS ( SELECT DISTINCT LTRIM(RTRIM(v.ProductCode)) AS ProductCode FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(ProductCode) WHERE LEN(LTRIM(RTRIM(v.ProductCode))) > 0 ), latest_base_price AS ( SELECT LTRIM(RTRIM(b.ItemCode)) AS ItemCode, CAST(b.Price AS DECIMAL(18, 2)) AS CostPrice, CONVERT(VARCHAR(10), b.PriceDate, 23) AS LastPricingDate, ROW_NUMBER() OVER ( PARTITION BY LTRIM(RTRIM(b.ItemCode)) ORDER BY b.PriceDate DESC, b.LastUpdatedDate DESC ) AS rn FROM prItemBasePrice b INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(b.ItemCode)) WHERE b.ItemTypeCode = 1 AND b.BasePriceCode = 1 AND LTRIM(RTRIM(b.CurrencyCode)) = 'USD' ), stock_entry_dates AS ( SELECT LTRIM(RTRIM(s.ItemCode)) AS ItemCode, CONVERT(VARCHAR(10), MAX(s.OperationDate), 23) AS StockEntryDate FROM trStock s WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) WHERE s.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 AND s.In_Qty1 > 0 AND LTRIM(RTRIM(s.InnerProcessCode)) = 'OP' AND LTRIM(RTRIM(s.WarehouseCode)) IN ( '1-0-14','1-0-10','1-0-8','1-2-5','1-2-4','1-0-12','100','1-0-28', '1-0-24','1-2-6','1-1-14','1-0-2','1-0-52','1-1-2','1-0-21','1-1-3', '1-0-33','101','1-014','1-0-49','1-0-36' ) GROUP BY LTRIM(RTRIM(s.ItemCode)) ), stock_base AS ( SELECT LTRIM(RTRIM(s.ItemCode)) AS ItemCode, SUM(s.In_Qty1 - s.Out_Qty1) AS InventoryQty1 FROM trStock s WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(s.ItemCode)) WHERE s.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(s.ItemCode))) = 13 GROUP BY LTRIM(RTRIM(s.ItemCode)) ), pick_base AS ( SELECT LTRIM(RTRIM(p.ItemCode)) AS ItemCode, SUM(p.Qty1) AS PickingQty1 FROM PickingStates p INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) WHERE p.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(p.ItemCode))) = 13 GROUP BY LTRIM(RTRIM(p.ItemCode)) ), reserve_base AS ( SELECT LTRIM(RTRIM(r.ItemCode)) AS ItemCode, SUM(r.Qty1) AS ReserveQty1 FROM ReserveStates r INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(r.ItemCode)) WHERE r.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(r.ItemCode))) = 13 GROUP BY LTRIM(RTRIM(r.ItemCode)) ), disp_base AS ( SELECT LTRIM(RTRIM(d.ItemCode)) AS ItemCode, SUM(d.Qty1) AS DispOrderQty1 FROM DispOrderStates d INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(d.ItemCode)) WHERE d.ItemTypeCode = 1 AND LEN(LTRIM(RTRIM(d.ItemCode))) = 13 GROUP BY LTRIM(RTRIM(d.ItemCode)) ) SELECT rc.ProductCode, COALESCE(lp.CostPrice, 0) AS CostPrice, CAST(ROUND( ISNULL(sb.InventoryQty1, 0) - ISNULL(pb.PickingQty1, 0) - ISNULL(rb.ReserveQty1, 0) - ISNULL(db.DispOrderQty1, 0) , 2) AS DECIMAL(18, 2)) AS StockQty, COALESCE(se.StockEntryDate, '') AS StockEntryDate, COALESCE(lp.LastPricingDate, '') AS LastPricingDate FROM req_codes rc LEFT JOIN latest_base_price lp ON lp.ItemCode = rc.ProductCode AND lp.rn = 1 LEFT JOIN stock_entry_dates se ON se.ItemCode = rc.ProductCode LEFT JOIN stock_base sb ON sb.ItemCode = rc.ProductCode LEFT JOIN pick_base pb ON pb.ItemCode = rc.ProductCode LEFT JOIN reserve_base rb ON rb.ItemCode = rc.ProductCode LEFT JOIN disp_base db ON db.ItemCode = rc.ProductCode; ` metricsRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...) if err != nil { return result, fmt.Errorf("metrics query failed: %w", err) } defer metricsRows.Close() type metrics struct { CostPrice float64 StockQty float64 StockEntryDate string LastPricingDate string } metricsByCode := make(map[string]metrics, len(out)) for metricsRows.Next() { var ( code string m metrics ) if err := metricsRows.Scan( &code, &m.CostPrice, &m.StockQty, &m.StockEntryDate, &m.LastPricingDate, ); err != nil { return result, err } metricsByCode[strings.TrimSpace(code)] = m } if err := metricsRows.Err(); err != nil { return result, err } for i := range out { if m, ok := metricsByCode[strings.TrimSpace(out[i].ProductCode)]; ok { out[i].CostPrice = m.CostPrice out[i].StockQty = m.StockQty out[i].StockEntryDate = m.StockEntryDate out[i].LastPricingDate = m.LastPricingDate } } result.Rows = out return result, nil } func isTransientMSSQLNetworkError(err error) bool { if err == nil { return false } e := strings.ToLower(err.Error()) return strings.Contains(e, "i/o timeout") || strings.Contains(e, "timeout") || strings.Contains(e, "wsarecv") || strings.Contains(e, "connection attempt failed") || strings.Contains(e, "no connection could be made") || strings.Contains(e, "broken pipe") || strings.Contains(e, "connection reset") }