package queries import ( "bssapp-backend/db" "bssapp-backend/models" "context" "database/sql" "fmt" "strconv" "strings" "time" ) func GetProductPricingList(ctx context.Context, limit int, afterProductCode string) ([]models.ProductPricing, error) { if limit <= 0 { limit = 500 } afterProductCode = strings.TrimSpace(afterProductCode) // Stage 1: fetch only paged products first (fast path). productQuery := ` SELECT TOP (` + strconv.Itoa(limit) + `) LTRIM(RTRIM(ProductCode)) AS ProductCode, 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 ProductAtt42 IN ('SERI', 'AKSESUAR') AND IsBlocked = 0 AND LEN(LTRIM(RTRIM(ProductCode))) = 13 AND (@p1 = '' OR LTRIM(RTRIM(ProductCode)) > @p1) ORDER BY LTRIM(RTRIM(ProductCode)); ` var ( rows *sql.Rows rowsErr error ) for attempt := 1; attempt <= 3; attempt++ { var err error rows, err = db.MssqlDB.QueryContext(ctx, productQuery, afterProductCode) 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 nil, rowsErr } defer rows.Close() out := make([]models.ProductPricing, 0, limit) for rows.Next() { var item models.ProductPricing if err := rows.Scan( &item.ProductCode, &item.AskiliYan, &item.Kategori, &item.UrunIlkGrubu, &item.UrunAnaGrubu, &item.UrunAltGrubu, &item.Icerik, &item.Karisim, &item.Marka, ); err != nil { return nil, err } out = append(out, item) } if err := rows.Err(); err != nil { return nil, err } if len(out) == 0 { return out, 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)) } codesCSV := strings.Join(codes, ",") metricsQuery := ` WITH req_codes AS ( SELECT DISTINCT LTRIM(RTRIM(value)) AS ProductCode FROM STRING_SPLIT(@p1, ',') WHERE LEN(LTRIM(RTRIM(value))) > 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.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, codesCSV) if err != nil { return nil, 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 nil, err } metricsByCode[strings.TrimSpace(code)] = m } if err := metricsRows.Err(); err != nil { return nil, 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 } } return out, 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") }