package queries import ( "bssapp-backend/db" "bssapp-backend/models" "context" "database/sql" "fmt" "math" "strconv" "strings" "time" "github.com/lib/pq" ) 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 GetAllProductPricingRows(ctx context.Context, chunkSize int, filters ProductPricingFilters, sortBy string, descending bool) ([]models.ProductPricing, error) { if chunkSize <= 0 || chunkSize > 1000 { chunkSize = 1000 } 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 ") sortBy = strings.TrimSpace(sortBy) orderDir := "DESC" if !descending { orderDir = "ASC" } orderExpr := "CAST(ROUND(ISNULL(sb.InventoryQty1, 0) - ISNULL(pb.PickingQty1, 0) - ISNULL(rb.ReserveQty1, 0) - ISNULL(db.DispOrderQty1, 0), 2) AS DECIMAL(18, 2))" if sortBy == "productCode" { orderExpr = "rc.ProductCode" orderDir = "ASC" } orderBySQL := orderExpr + ` ` + orderDir if !strings.EqualFold(strings.TrimSpace(orderExpr), "rc.ProductCode") { orderBySQL += `, rc.ProductCode ASC` } baseQuery := ` 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; IF OBJECT_ID('tempdb..#pick_base') IS NOT NULL DROP TABLE #pick_base; IF OBJECT_ID('tempdb..#reserve_base') IS NOT NULL DROP TABLE #reserve_base; IF OBJECT_ID('tempdb..#disp_base') IS NOT NULL DROP TABLE #disp_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, MAX(f.BrandCode) AS BrandCode 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, COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode 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 LTRIM(RTRIM(p.ItemCode)) AS ItemCode, SUM(p.Qty1) AS PickingQty1 INTO #pick_base 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)); SELECT LTRIM(RTRIM(r.ItemCode)) AS ItemCode, SUM(r.Qty1) AS ReserveQty1 INTO #reserve_base 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)); SELECT LTRIM(RTRIM(d.ItemCode)) AS ItemCode, SUM(d.Qty1) AS DispOrderQty1 INTO #disp_base 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, rc.BrandGroupSec, rc.AskiliYan, rc.Kategori, rc.UrunIlkGrubu, rc.UrunAnaGrubu, rc.UrunAltGrubu, rc.Icerik, rc.Karisim, rc.Marka, rc.BrandCode, 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 FROM #req_codes rc 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 ORDER BY ` + orderBySQL + `; ` rows, err := db.MssqlDB.QueryContext(ctx, baseQuery, args...) if err != nil { return nil, err } defer rows.Close() out := make([]models.ProductPricing, 0, 2048) 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, &item.BrandCode, &item.StockQty, ); 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 } if err := enrichAllProductPricingRows(ctx, out, chunkSize); err != nil { return nil, err } return out, nil } func enrichAllProductPricingRows(ctx context.Context, out []models.ProductPricing, chunkSize int) error { if len(out) == 0 { return nil } if chunkSize <= 0 || chunkSize > 1000 { chunkSize = 1000 } indexByCode := make(map[string]int, len(out)) codes := make([]string, 0, len(out)) for i := range out { code := strings.TrimSpace(out[i].ProductCode) if code == "" { continue } indexByCode[code] = i codes = append(codes, code) } for _, chunk := range chunkStringSlice(codes, chunkSize) { valueRows := make([]string, 0, len(chunk)) metricArgs := make([]any, 0, len(chunk)) for i, code := range chunk { 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_pricelist_line AS ( SELECT LTRIM(RTRIM(p.ItemCode)) AS ItemCode, LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode, CAST(p.Price AS DECIMAL(18, 2)) AS Price, ROW_NUMBER() OVER ( PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode)) ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC ) AS rn FROM dbo.trPriceListLine p WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) WHERE p.ItemTypeCode = 1 AND ISNULL(p.IsDisabled, 0) = 0 AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY') AND ( (LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD') OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY') ) AND p.Price IS NOT NULL AND p.Price > 0 ), base_prices AS ( SELECT ItemCode, MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END) AS BasePriceUsd, MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END) AS BasePriceTry FROM latest_pricelist_line WHERE rn = 1 GROUP BY ItemCode ), 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)) ) SELECT rc.ProductCode, COALESCE(lp.CostPrice, 0) AS CostPrice, COALESCE(bp.BasePriceUsd, 0) AS BasePriceUsd, COALESCE(bp.BasePriceTry, 0) AS BasePriceTry, 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 base_prices bp ON bp.ItemCode = rc.ProductCode LEFT JOIN stock_entry_dates se ON se.ItemCode = rc.ProductCode; ` metricRows, err := db.MssqlDB.QueryContext(ctx, metricsQuery, metricArgs...) if err != nil { return fmt.Errorf("metrics query failed: %w", err) } for metricRows.Next() { var code string var costPrice, basePriceUsd, basePriceTry float64 var stockEntryDate, lastPricingDate string if err := metricRows.Scan(&code, &costPrice, &basePriceUsd, &basePriceTry, &stockEntryDate, &lastPricingDate); err != nil { _ = metricRows.Close() return err } if idx, ok := indexByCode[strings.TrimSpace(code)]; ok { out[idx].CostPrice = costPrice out[idx].BasePriceUsd = basePriceUsd out[idx].BasePriceTry = basePriceTry out[idx].StockEntryDate = stockEntryDate out[idx].LastPricingDate = lastPricingDate } } if err := metricRows.Err(); err != nil { _ = metricRows.Close() return err } _ = metricRows.Close() if uretimDB := db.GetUretimDB(); uretimDB != nil { costingQuery := ` 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 ) SELECT LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu, CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate FROM dbo.spUrtOnMLMas m WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu)) GROUP BY LTRIM(RTRIM(m.UrunKodu)); ` costRows, err := uretimDB.QueryContext(ctx, costingQuery, metricArgs...) if err == nil { for costRows.Next() { var code, d string if err := costRows.Scan(&code, &d); err != nil { _ = costRows.Close() return err } if idx, ok := indexByCode[strings.TrimSpace(code)]; ok && strings.TrimSpace(d) != "" { out[idx].LastCostingDate = strings.TrimSpace(d) } } if err := costRows.Err(); err != nil { _ = costRows.Close() return err } _ = costRows.Close() } } if pg := db.PgDB; pg != nil { tierSQL := ` WITH ranked AS ( SELECT mmitem.code AS code, sdprc.sdprcgrp_id AS grp, sdprc.crn AS crn, COALESCE(sdprc.prc, 0) AS prc, ROW_NUMBER() OVER ( PARTITION BY mmitem.code, sdprc.crn, sdprc.sdprcgrp_id ORDER BY sdprc.zlins_dttm DESC ) AS rn FROM sdprc JOIN mmitem ON mmitem.id = sdprc.mmitem_id WHERE mmitem.code = ANY($1) AND sdprc.sdprcgrp_id BETWEEN 1 AND 6 AND sdprc.crn IN ('USD', 'EUR', 'TRY') AND sdprc.prc IS NOT NULL AND sdprc.prc > 0 ) SELECT code, grp, crn, prc FROM ranked WHERE rn = 1; ` pgRows, err := pg.QueryContext(ctx, tierSQL, pq.Array(chunk)) if err == nil { for pgRows.Next() { var code, crn string var grp int var prc float64 if err := pgRows.Scan(&code, &grp, &crn, &prc); err != nil { _ = pgRows.Close() return err } idx, ok := indexByCode[strings.TrimSpace(code)] if !ok { continue } switch strings.ToUpper(strings.TrimSpace(crn)) { case "USD": switch grp { case 1: out[idx].USD1 = prc case 2: out[idx].USD2 = prc case 3: out[idx].USD3 = prc case 4: out[idx].USD4 = prc case 5: out[idx].USD5 = prc case 6: out[idx].USD6 = prc } case "EUR": switch grp { case 1: out[idx].EUR1 = prc case 2: out[idx].EUR2 = prc case 3: out[idx].EUR3 = prc case 4: out[idx].EUR4 = prc case 5: out[idx].EUR5 = prc case 6: out[idx].EUR6 = prc } case "TRY": switch grp { case 1: out[idx].TRY1 = prc case 2: out[idx].TRY2 = prc case 3: out[idx].TRY3 = prc case 4: out[idx].TRY4 = prc case 5: out[idx].TRY5 = prc case 6: out[idx].TRY6 = prc } } } if err := pgRows.Err(); err != nil { _ = pgRows.Close() return err } _ = pgRows.Close() } // Last pricing date should reflect the most recent price publish, not the Nebim base price date. // E-commerce reads pg.sdprc; use its latest write timestamp as the authoritative "last pricing" signal. dateRows, err := pg.QueryContext(ctx, ` SELECT mmitem.code AS code, to_char(MAX(sdprc.zlins_dttm), 'YYYY-MM-DD') AS last_pricing_date FROM sdprc JOIN mmitem ON mmitem.id = sdprc.mmitem_id WHERE mmitem.code = ANY($1) GROUP BY mmitem.code; `, pq.Array(chunk)) if err == nil { for dateRows.Next() { var code, ymd string if err := dateRows.Scan(&code, &ymd); err != nil { _ = dateRows.Close() return err } code = strings.TrimSpace(code) ymd = strings.TrimSpace(ymd) if code == "" || len(ymd) != 10 { continue } if idx, ok := indexByCode[code]; ok { cur := strings.TrimSpace(out[idx].LastPricingDate) // both are YYYY-MM-DD, lexicographical compare is safe if cur == "" || cur < ymd { out[idx].LastPricingDate = ymd } } } if err := dateRows.Err(); err != nil { _ = dateRows.Close() return err } _ = dateRows.Close() } } } if pg := db.PgDB; pg != nil { brandCodes := make([]string, 0, len(out)) seen := make(map[string]struct{}, len(out)) for _, it := range out { code := strings.TrimSpace(it.BrandCode) if code == "" { continue } if _, ok := seen[code]; ok { continue } seen[code] = struct{}{} brandCodes = append(brandCodes, code) } for _, chunk := range chunkStringSlice(brandCodes, chunkSize) { rows, err := pg.QueryContext(ctx, ` SELECT m.brand_code, COALESCE(g.title, '') AS grp_title FROM mk_brandgrpmatch m JOIN mk_brandgrp g ON g.id = m.grp_id WHERE m.brand_code = ANY($1) `, pq.Array(chunk)) if err != nil { continue } grpByBrand := make(map[string]string, len(chunk)) for rows.Next() { var code, title string if err := rows.Scan(&code, &title); err != nil { _ = rows.Close() return err } grpByBrand[strings.TrimSpace(code)] = strings.TrimSpace(title) } if err := rows.Err(); err != nil { _ = rows.Close() return err } _ = rows.Close() for i := range out { if title, ok := grpByBrand[strings.TrimSpace(out[i].BrandCode)]; ok { out[i].BrandGroupSec = title } } } } return nil } func chunkStringSlice(values []string, size int) [][]string { if size <= 0 { size = 1000 } out := make([][]string, 0, (len(values)+size-1)/size) for start := 0; start < len(values); start += size { end := start + size if end > len(values) { end = len(values) } out = append(out, values[start:end]) } return out } func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (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 ") if includeTotal { 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 } else { // Skip COUNT(*) for performance; client will infer hasMore from page size. result.TotalCount = 0 result.TotalPages = 0 result.Page = page result.Limit = limit } // Stage 1: fetch only paged products first (fast path). sortBy = strings.TrimSpace(sortBy) orderDir := "DESC" if !descending { orderDir = "ASC" } // Only allow a small safe list. orderExpr := "CAST(ROUND(ISNULL(sb.InventoryQty1, 0), 2) AS DECIMAL(18, 2))" if sortBy == "productCode" { orderExpr = "rc.ProductCode" orderDir = "ASC" } orderBySQL := orderExpr + ` ` + orderDir if !strings.EqualFold(strings.TrimSpace(orderExpr), "rc.ProductCode") { orderBySQL += `, rc.ProductCode ASC` } 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, MAX(f.BrandCode) AS BrandCode 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, COALESCE(LTRIM(RTRIM(ProductAtt10)), '') AS BrandCode 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, rc.BrandCode FROM #req_codes rc LEFT JOIN #stock_base sb ON sb.ItemCode = rc.ProductCode ORDER BY ` + orderBySQL + ` 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, &item.BrandCode, ); 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_pricelist_line AS ( -- Base prices from Nebim V3 price lists (trPriceListLine). -- Pick the latest record per (ItemCode, Currency) using ValidDate/ValidTime, then LastUpdatedDate. SELECT LTRIM(RTRIM(p.ItemCode)) AS ItemCode, LTRIM(RTRIM(p.DocCurrencyCode)) AS DocCurrencyCode, CAST(p.Price AS DECIMAL(18, 2)) AS Price, ROW_NUMBER() OVER ( PARTITION BY LTRIM(RTRIM(p.ItemCode)), LTRIM(RTRIM(p.DocCurrencyCode)) ORDER BY p.ValidDate DESC, p.ValidTime DESC, p.LastUpdatedDate DESC ) AS rn FROM dbo.trPriceListLine p WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(p.ItemCode)) WHERE p.ItemTypeCode = 1 AND ISNULL(p.IsDisabled, 0) = 0 AND LTRIM(RTRIM(p.DocCurrencyCode)) IN ('USD', 'TRY') AND ( (LTRIM(RTRIM(p.DocCurrencyCode)) = 'USD' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-USD') OR (LTRIM(RTRIM(p.DocCurrencyCode)) = 'TRY' AND LTRIM(RTRIM(p.PriceGroupCode)) = 'TM-TRY') ) AND p.Price IS NOT NULL AND p.Price > 0 ), base_prices AS ( SELECT ItemCode, MAX(CASE WHEN DocCurrencyCode = 'USD' THEN Price END) AS BasePriceUsd, MAX(CASE WHEN DocCurrencyCode = 'TRY' THEN Price END) AS BasePriceTry FROM latest_pricelist_line WHERE rn = 1 GROUP BY ItemCode ), 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, COALESCE(bp.BasePriceUsd, 0) AS BasePriceUsd, COALESCE(bp.BasePriceTry, 0) AS BasePriceTry, 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, '' AS LastCostingDate, 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 base_prices bp ON bp.ItemCode = rc.ProductCode 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 BasePriceUsd float64 BasePriceTry float64 StockQty float64 StockEntryDate string LastCostingDate 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.BasePriceUsd, &m.BasePriceTry, &m.StockQty, &m.StockEntryDate, &m.LastCostingDate, &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].BasePriceUsd = m.BasePriceUsd out[i].BasePriceTry = m.BasePriceTry out[i].StockQty = m.StockQty out[i].StockEntryDate = m.StockEntryDate out[i].LastCostingDate = m.LastCostingDate out[i].LastPricingDate = m.LastPricingDate } } // Stage 3: fetch costing date from UretimDB (separate MSSQL catalog). // Pricing DB may not contain spUrtOnMLMas; do not fail listing on costing query errors. if uretimDB := db.GetUretimDB(); uretimDB != nil { costingQuery := ` 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 ) SELECT LTRIM(RTRIM(m.UrunKodu)) AS UrunKodu, CONVERT(VARCHAR(10), MAX(m.Tarihi), 23) AS LastCostingDate FROM dbo.spUrtOnMLMas m WITH(NOLOCK) INNER JOIN req_codes rc ON rc.ProductCode = LTRIM(RTRIM(m.UrunKodu)) GROUP BY LTRIM(RTRIM(m.UrunKodu)); ` costRows, err := uretimDB.QueryContext(ctx, costingQuery, metricArgs...) if err == nil { costingByCode := make(map[string]string, len(out)) for costRows.Next() { var code, d string if err := costRows.Scan(&code, &d); err != nil { _ = costRows.Close() costRows = nil break } costingByCode[strings.TrimSpace(code)] = strings.TrimSpace(d) } if costRows != nil { _ = costRows.Close() for i := range out { if d, ok := costingByCode[strings.TrimSpace(out[i].ProductCode)]; ok && d != "" { out[i].LastCostingDate = d } } } } } // Stage 4: fetch latest tier prices (USD1..6, EUR1..6, TRY1..6) from PostgreSQL sdprc/mmitem. if pg := db.PgDB; pg != nil { type tierRow struct { Code string Grp int Crn string Prc float64 } tierSQL := ` WITH ranked AS ( SELECT mmitem.code AS code, sdprc.sdprcgrp_id AS grp, sdprc.crn AS crn, COALESCE(sdprc.prc, 0) AS prc, ROW_NUMBER() OVER ( PARTITION BY mmitem.code, sdprc.crn, sdprc.sdprcgrp_id ORDER BY sdprc.zlins_dttm DESC ) AS rn FROM sdprc JOIN mmitem ON mmitem.id = sdprc.mmitem_id WHERE mmitem.code = ANY($1) AND sdprc.sdprcgrp_id BETWEEN 1 AND 6 AND sdprc.crn IN ('USD', 'EUR', 'TRY') AND sdprc.prc IS NOT NULL AND sdprc.prc > 0 ) SELECT code, grp, crn, prc FROM ranked WHERE rn = 1; ` rows, err := pg.QueryContext(ctx, tierSQL, pq.Array(codes)) if err == nil { defer rows.Close() type key struct { Code string } tiers := make(map[string]map[string]map[int]float64, len(out)) for rows.Next() { var r tierRow if err := rows.Scan(&r.Code, &r.Grp, &r.Crn, &r.Prc); err != nil { break } r.Code = strings.TrimSpace(r.Code) r.Crn = strings.TrimSpace(strings.ToUpper(r.Crn)) if r.Code == "" || r.Grp < 1 || r.Grp > 6 || r.Crn == "" { continue } if _, ok := tiers[r.Code]; !ok { tiers[r.Code] = map[string]map[int]float64{} } if _, ok := tiers[r.Code][r.Crn]; !ok { tiers[r.Code][r.Crn] = map[int]float64{} } tiers[r.Code][r.Crn][r.Grp] = r.Prc } for i := range out { code := strings.TrimSpace(out[i].ProductCode) m, ok := tiers[code] if !ok { continue } apply := func(crn string, grp int, v float64) { switch crn { case "USD": switch grp { case 1: out[i].USD1 = v case 2: out[i].USD2 = v case 3: out[i].USD3 = v case 4: out[i].USD4 = v case 5: out[i].USD5 = v case 6: out[i].USD6 = v } case "EUR": switch grp { case 1: out[i].EUR1 = v case 2: out[i].EUR2 = v case 3: out[i].EUR3 = v case 4: out[i].EUR4 = v case 5: out[i].EUR5 = v case 6: out[i].EUR6 = v } case "TRY": switch grp { case 1: out[i].TRY1 = v case 2: out[i].TRY2 = v case 3: out[i].TRY3 = v case 4: out[i].TRY4 = v case 5: out[i].TRY5 = v case 6: out[i].TRY6 = v } } } for crn, byGrp := range m { for grp, v := range byGrp { apply(crn, grp, v) } } } } } // Stage 5: brand group (classification) from Postgres mk_brandgrpmatch. // Show classification result in BrandGroupSec field (read-only in UI). if pg := db.PgDB; pg != nil { brandCodes := make([]string, 0, len(out)) seen := make(map[string]struct{}, len(out)) for _, it := range out { code := strings.TrimSpace(it.BrandCode) if code == "" { continue } if _, ok := seen[code]; ok { continue } seen[code] = struct{}{} brandCodes = append(brandCodes, code) } if len(brandCodes) > 0 { rows, err := pg.QueryContext(ctx, ` SELECT m.brand_code, COALESCE(g.title, '') AS grp_title FROM mk_brandgrpmatch m JOIN mk_brandgrp g ON g.id = m.grp_id WHERE m.brand_code = ANY($1) `, pq.Array(brandCodes)) if err == nil { defer rows.Close() grpByBrand := make(map[string]string, len(brandCodes)) for rows.Next() { var code, title string if err := rows.Scan(&code, &title); err != nil { break } grpByBrand[strings.TrimSpace(code)] = strings.TrimSpace(title) } for i := range out { if title, ok := grpByBrand[strings.TrimSpace(out[i].BrandCode)]; ok { out[i].BrandGroupSec = title } else { out[i].BrandGroupSec = "" } } } } } 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") }