diff --git a/svc/queries/product_pricing.go b/svc/queries/product_pricing.go
index b3d1faa..de6a756 100644
--- a/svc/queries/product_pricing.go
+++ b/svc/queries/product_pricing.go
@@ -672,6 +672,20 @@ func cleanProductPricingFilterValues(values []string) []string {
return clean
}
+func isProductCodeOnlyPricingFilter(filters ProductPricingFilters) bool {
+ return len(cleanProductPricingFilterValues(filters.ProductCode)) > 0 &&
+ strings.TrimSpace(filters.Search) == "" &&
+ len(cleanProductPricingFilterValues(filters.BrandGroup)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.AskiliYan)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.Kategori)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.UrunIlkGrubu)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.UrunAnaGrubu)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.UrunAltGrubu)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.Icerik)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.Karisim)) == 0 &&
+ len(cleanProductPricingFilterValues(filters.Marka)) == 0
+}
+
func GetProductPricingPage(ctx context.Context, page int, limit int, filters ProductPricingFilters, includeTotal bool, sortBy string, descending bool) (ProductPricingPage, error) {
result := ProductPricingPage{
Rows: []models.ProductPricing{},
@@ -751,8 +765,28 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
}, " OR ")+")")
}
whereSQL := strings.Join(whereParts, " AND ")
+ cleanProductCodes := cleanProductPricingFilterValues(filters.ProductCode)
+ productCodeOnlyFilter := isProductCodeOnlyPricingFilter(filters)
- if includeTotal {
+ if includeTotal && productCodeOnlyFilter {
+ result.TotalCount = len(cleanProductCodes)
+ if result.TotalCount == 0 {
+ result.TotalPages = 0
+ result.Page = 1
+ return result, nil
+ }
+ totalPages := int(math.Ceil(float64(result.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 if includeTotal {
countQuery := `
SELECT COUNT(DISTINCT LTRIM(RTRIM(ProductCode)))
FROM ProductFilterWithDescription('TR')
@@ -789,7 +823,7 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
// Stage 1: fetch only paged products first. Exact product-code filters do not
// need the stock sort temp table here; detailed metrics are fetched below.
- productCodeFastPath := len(cleanProductPricingFilterValues(filters.ProductCode)) > 0
+ productCodeFastPath := len(cleanProductCodes) > 0
sortBy = strings.TrimSpace(sortBy)
orderDir := "DESC"
if !descending {
@@ -876,7 +910,72 @@ func GetProductPricingPage(ctx context.Context, page int, limit int, filters Pro
OFFSET ` + strconv.Itoa(offset) + ` ROWS
FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
`
- if productCodeFastPath {
+ if productCodeOnlyFilter {
+ valueRows := make([]string, 0, len(cleanProductCodes))
+ directArgs := make([]any, 0, len(cleanProductCodes))
+ for i, code := range cleanProductCodes {
+ paramName := "@p" + strconv.Itoa(i+1)
+ valueRows = append(valueRows, "("+paramName+")")
+ directArgs = append(directArgs, code)
+ }
+ args = directArgs
+ productQuery = `
+ 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
+ ),
+ attr AS (
+ SELECT
+ LTRIM(RTRIM(a.ItemCode)) AS ProductCode,
+ MAX(CASE WHEN a.AttributeTypeCode = 45 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS AskiliYan,
+ MAX(CASE WHEN a.AttributeTypeCode = 44 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Kategori,
+ MAX(CASE WHEN a.AttributeTypeCode = 42 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS UrunIlkGrubuCode,
+ MAX(CASE WHEN a.AttributeTypeCode = 42 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunIlkGrubu,
+ MAX(CASE WHEN a.AttributeTypeCode = 1 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAnaGrubu,
+ MAX(CASE WHEN a.AttributeTypeCode = 2 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS UrunAltGrubu,
+ MAX(CASE WHEN a.AttributeTypeCode = 41 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Icerik,
+ MAX(CASE WHEN a.AttributeTypeCode = 29 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Karisim,
+ MAX(CASE WHEN a.AttributeTypeCode = 10 THEN COALESCE(NULLIF(LTRIM(RTRIM(d.AttributeDescription)), ''), LTRIM(RTRIM(a.AttributeCode))) ELSE '' END) AS Marka,
+ MAX(CASE WHEN a.AttributeTypeCode = 10 THEN LTRIM(RTRIM(a.AttributeCode)) ELSE '' END) AS BrandCode
+ FROM dbo.prItemAttribute a WITH(NOLOCK)
+ INNER JOIN req_codes rc
+ ON rc.ProductCode = LTRIM(RTRIM(a.ItemCode))
+ LEFT JOIN dbo.cdItemAttributeDesc d WITH(NOLOCK)
+ ON d.ItemTypeCode = a.ItemTypeCode
+ AND d.AttributeTypeCode = a.AttributeTypeCode
+ AND d.AttributeCode = a.AttributeCode
+ AND d.LangCode = 'TR'
+ WHERE a.ItemTypeCode = 1
+ AND a.AttributeTypeCode IN (1,2,10,29,41,42,44,45)
+ GROUP BY LTRIM(RTRIM(a.ItemCode))
+ )
+ SELECT
+ rc.ProductCode,
+ CAST('' AS NVARCHAR(100)) AS BrandGroupSec,
+ COALESCE(attr.AskiliYan, '') AS AskiliYan,
+ COALESCE(attr.Kategori, '') AS Kategori,
+ COALESCE(attr.UrunIlkGrubu, '') AS UrunIlkGrubu,
+ COALESCE(attr.UrunAnaGrubu, '') AS UrunAnaGrubu,
+ COALESCE(attr.UrunAltGrubu, '') AS UrunAltGrubu,
+ COALESCE(attr.Icerik, '') AS Icerik,
+ COALESCE(attr.Karisim, '') AS Karisim,
+ COALESCE(attr.Marka, '') AS Marka,
+ COALESCE(attr.BrandCode, '') AS BrandCode
+ FROM req_codes rc
+ INNER JOIN dbo.cdItem ci WITH(NOLOCK)
+ ON ci.ItemTypeCode = 1
+ AND LTRIM(RTRIM(ci.ItemCode)) = rc.ProductCode
+ LEFT JOIN attr
+ ON attr.ProductCode = rc.ProductCode
+ WHERE ISNULL(ci.IsBlocked, 0) = 0
+ AND LEN(LTRIM(RTRIM(ci.ItemCode))) = 13
+ AND COALESCE(attr.UrunIlkGrubuCode, '') IN ('SERI', 'AKSESUAR')
+ ORDER BY rc.ProductCode ASC
+ OFFSET ` + strconv.Itoa(offset) + ` ROWS
+ FETCH NEXT ` + strconv.Itoa(limit) + ` ROWS ONLY;
+ `
+ } else if productCodeFastPath {
productQuery = `
IF OBJECT_ID('tempdb..#req_codes') IS NOT NULL DROP TABLE #req_codes;
diff --git a/svc/queries/product_pricing_fx_publish.go b/svc/queries/product_pricing_fx_publish.go
index 0217fa1..cab81de 100644
--- a/svc/queries/product_pricing_fx_publish.go
+++ b/svc/queries/product_pricing_fx_publish.go
@@ -258,8 +258,9 @@ norm AS (
COALESCE(price, 0) AS price
FROM input
),
- -- Prefer PG's authoritative variant dimension table (mmitem_dim). Fall back to cache table if needed.
- dims_mmitem_dim AS (
+ -- Only PG's authoritative variant dimension table may drive delta writes.
+ -- Do not union sdprc/cache dimensions; stale rows there can re-create wrong variant keys.
+ dims AS (
SELECT
norm.product_code AS product_code,
md.val1::bigint AS dim1,
@@ -275,41 +276,7 @@ norm AS (
AND COALESCE(md.is_active, TRUE) = TRUE
WHERE md.val1 IS NOT NULL
AND md.val1 > 0
- GROUP BY norm.product_code, md.val1, md.val2
- ),
- dims_cache_table AS (
- SELECT
- NULLIF(BTRIM(c.product_code), '') AS product_code,
- c.dim1::bigint AS dim1,
- c.dim3::bigint AS dim3
- FROM mk_mmitem_dim_combo c
- JOIN norm
- ON norm.product_code = c.product_code
- WHERE c.dim1 IS NOT NULL
- ),
- dims_cache AS (
- SELECT product_code, dim1, dim3 FROM dims_mmitem_dim
- UNION
- SELECT product_code, dim1, dim3 FROM dims_cache_table
- ),
- dims_sdprc AS (
- SELECT
- norm.product_code AS product_code,
- s.dim1 AS dim1,
- s.dim3 AS dim3
- FROM norm
- JOIN mmitem mm
- ON mm.code = norm.product_code
- JOIN sdprc s
- ON s.mmitem_id = mm.id
- WHERE s.dim1 IS NOT NULL
- AND s.dim1 > 0
- GROUP BY norm.product_code, s.dim1, s.dim3
- ),
- dims AS (
- SELECT product_code, dim1, dim3 FROM dims_cache
- UNION
- SELECT product_code, dim1, dim3 FROM dims_sdprc
+ GROUP BY norm.product_code, md.val1, md.val3
),
mapped AS (
SELECT
diff --git a/svc/routes/product_pricing_save.go b/svc/routes/product_pricing_save.go
index 947d9ed..35483c9 100644
--- a/svc/routes/product_pricing_save.go
+++ b/svc/routes/product_pricing_save.go
@@ -431,37 +431,6 @@ WHERE is_active = TRUE
return out
}
- loadDimCombosFromCache := func(productCode string) ([]dimCombo, error) {
- productCode = strings.TrimSpace(productCode)
- if productCode == "" {
- return nil, nil
- }
- rows, err := pgTx.QueryContext(ctx, `
-SELECT dim1, dim3
-FROM mk_mmitem_dim_combo
-WHERE product_code = $1
-ORDER BY dim1, dim3_key
-`, productCode)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
-
- out := make([]dimCombo, 0, 32)
- for rows.Next() {
- var d1 int64
- var d3 sql.NullInt64
- if err := rows.Scan(&d1, &d3); err != nil {
- return nil, err
- }
- if d1 <= 0 {
- continue
- }
- out = append(out, dimCombo{Dim1: d1, Dim3: d3})
- }
- return out, rows.Err()
- }
-
parseDimID := func(s string) (int64, bool) {
s = strings.TrimSpace(s)
if s == "" {
@@ -812,7 +781,7 @@ filtered AS (
AND price > 0
),
grouped AS (
- -- Ensure one row per business key to avoid unique violations under strict constraints (e.g. uq_sdprc_3).
+ -- Ensure one row per business key to avoid unique violations under strict constraints.
SELECT
sdprcgrp_id,
currency AS crn,
@@ -822,16 +791,35 @@ grouped AS (
FROM filtered
GROUP BY sdprcgrp_id, currency, dim1, dim3
),
-upserted AS (
+updated AS (
+ UPDATE sdprc s
+ SET prc = g.prc,
+ zlins_dttm = now()
+ FROM grouped g
+ WHERE s.mmitem_id = $2::bigint
+ AND s.sdprcgrp_id = g.sdprcgrp_id
+ AND s.crn = g.crn
+ AND s.dim1 = g.dim1
+ AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0)
+ AND s.prc IS DISTINCT FROM g.prc
+ RETURNING 1
+),
+inserted AS (
INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm)
SELECT $2::bigint, g.sdprcgrp_id, g.crn, g.dim1, g.dim3, g.prc, now()
FROM grouped g
- ON CONFLICT ON CONSTRAINT uq_sdprc_3
- DO UPDATE SET prc = EXCLUDED.prc, zlins_dttm = EXCLUDED.zlins_dttm
- WHERE sdprc.prc IS DISTINCT FROM EXCLUDED.prc
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM sdprc s
+ WHERE s.mmitem_id = $2::bigint
+ AND s.sdprcgrp_id = g.sdprcgrp_id
+ AND s.crn = g.crn
+ AND s.dim1 = g.dim1
+ AND COALESCE(s.dim3, 0) = COALESCE(g.dim3, 0)
+ )
RETURNING 1
)
-SELECT COUNT(*)::int FROM upserted;
+SELECT ((SELECT COUNT(*) FROM updated) + (SELECT COUNT(*) FROM inserted))::int;
`
var inserted int
if err := pgTx.QueryRowContext(ctx, q, raw, mmItemID).Scan(&inserted); err != nil {
@@ -1083,28 +1071,8 @@ VALUES (
_ = upsertDimCombosCache(code, dims) // best-effort cache fill
}
- // 2) Cache fallback (fast).
- cacheStarted := time.Now()
- if len(dims) == 0 {
- cached, cacheErr := loadDimCombosFromCache(code)
- if cacheErr == nil && len(cached) > 0 {
- dims = cached
- logger.Info("save:pg:dims:cache:hit",
- "product_code", code,
- "dims", len(dims),
- "duration_ms", time.Since(cacheStarted).Milliseconds(),
- )
- } else if cacheErr != nil {
- logger.Error("save:pg:dims:cache-load:error", "product_code", code, "err", cacheErr)
- } else {
- logger.Info("save:pg:dims:cache:miss",
- "product_code", code,
- "duration_ms", time.Since(cacheStarted).Milliseconds(),
- )
- }
- }
-
- // 3) Last resort: MSSQL stock tokens (legacy).
+ // 2) Last resort: MSSQL stock tokens, then seed mmitem_dim. Do not use
+ // mk_mmitem_dim_combo as a write source; stale cache rows can create wrong keys.
if len(dims) == 0 {
d, err := loadDimsFromMssqlStock(code)
if err != nil {
diff --git a/ui/src/pages/ProductPricing.vue b/ui/src/pages/ProductPricing.vue
index 329c51f..8850573 100644
--- a/ui/src/pages/ProductPricing.vue
+++ b/ui/src/pages/ProductPricing.vue
@@ -3,6 +3,18 @@