diff --git a/svc/product_series_auto_scheduler.go b/svc/product_series_auto_scheduler.go index 650ffd6..45aa158 100644 --- a/svc/product_series_auto_scheduler.go +++ b/svc/product_series_auto_scheduler.go @@ -1149,12 +1149,33 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode, } return 0, 0, sql.NullInt64{}, false, err } - dim1ID, ok, err := productSeriesResolveDimTokenID(ctx, pg, "dimval1", colorCode, mmitemID) - if err != nil { - return 0, 0, sql.NullInt64{}, false, err - } - if !ok || dim1ID <= 0 { - return 0, 0, sql.NullInt64{}, false, nil + // Authoritative dim1 resolver: for this installation, "color_code" tokens correspond to dfgrp.code on + // the dim values referenced by mmitem_dim.val1. Do NOT rely on mk_dim_token_map for dimval1 because + // it can be polluted and conflate tokens. + var dim1ID int64 + { + tok := strings.ToUpper(strings.TrimSpace(colorCode)) + if tok == "" || tok == "0" { + return 0, 0, sql.NullInt64{}, false, nil + } + if err := pg.QueryRowContext(ctx, ` +SELECT md.val1 +FROM mmitem_dim md +JOIN dfgrp d ON d.id = md.val1 +WHERE md.mmitem_id = $1 + AND md.is_active = TRUE + AND UPPER(BTRIM(d.code)) = $2 +GROUP BY md.val1 +LIMIT 1 +`, mmitemID, tok).Scan(&dim1ID); err != nil { + if err == sql.ErrNoRows { + return 0, 0, sql.NullInt64{}, false, nil + } + return 0, 0, sql.NullInt64{}, false, err + } + if dim1ID <= 0 { + return 0, 0, sql.NullInt64{}, false, nil + } } var dim3ID sql.NullInt64 if strings.TrimSpace(dim3Code) != "" { diff --git a/svc/routes/product_series.go b/svc/routes/product_series.go index 98248df..761d976 100644 --- a/svc/routes/product_series.go +++ b/svc/routes/product_series.go @@ -318,42 +318,10 @@ WHERE dim_column=$1 AND token = ANY($2) return out, rows.Err() } - // Load authoritative combos from mmitem_dim (mmitem_id + val1(color) + val3(dim3 if any)). - // We do NOT rely on mk_mmitem_dim_combo here because that cache may be empty/stale. - loadProductDimCombos := func(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) { - out := map[string]struct{}{} - if len(mmitemIDs) == 0 { - return out, nil - } - rows, err := pg.QueryContext(ctx, ` -SELECT - mmitem_id, - val1 AS dim1, - CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key -FROM mmitem_dim -WHERE mmitem_id = ANY($1) - AND is_active = TRUE - AND val1 IS NOT NULL -GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END -`, pq.Array(mmitemIDs)) - if err != nil { - return out, err - } - defer rows.Close() - for rows.Next() { - var mmitemID, dim1, dim3Key int64 - if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil { - return out, err - } - key := fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key) - out[key] = struct{}{} - } - return out, rows.Err() - } + // Authoritative dims/combos are resolved via mmitem_dim + dfgrp.code (see helpers below). codes := setToSortedSlice(codeSet) mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes) - dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet)) dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set)) mmitemIDs := make([]int64, 0, len(mmitemByCode)) for _, c := range codes { @@ -361,7 +329,13 @@ GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 E mmitemIDs = append(mmitemIDs, id) } } - combos, _ := loadProductDimCombos(ctx, pg, mmitemIDs) + // Normalize color tokens to UPPER/TRIM to match dfgrp.code. + colorTokens := setToSortedSlice(colorSet) + for i := range colorTokens { + colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i])) + } + dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens) + combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs) existing, _ := loadProductSeriesAssignments(ctx, pg, codes) // Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans. @@ -425,7 +399,7 @@ LIMIT 1 out := make([]productSeriesMappingRow, 0, len(grouped)) for _, row := range grouped { row.MmitemID = mmitemByCode[row.ProductCode] - row.Dim1ID = dim1ByToken[row.ColorCode] + row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))] if row.Dim3Code != "" { // Prefer token map; fallback to per-item inference (do not persist). if v := dim3ByToken[row.Dim3Code]; v > 0 { @@ -621,40 +595,8 @@ WHERE dim_column=$1 AND token = ANY($2) return out, rows.Err() } - loadProductDimCombos := func(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) { - out := map[string]struct{}{} - if len(mmitemIDs) == 0 { - return out, nil - } - rows, err := pg.QueryContext(ctx, ` -SELECT - mmitem_id, - val1 AS dim1, - CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key -FROM mmitem_dim -WHERE mmitem_id = ANY($1) - AND is_active = TRUE - AND val1 IS NOT NULL -GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END -`, pq.Array(mmitemIDs)) - if err != nil { - return out, err - } - defer rows.Close() - for rows.Next() { - var mmitemID, dim1, dim3Key int64 - if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil { - return out, err - } - key := fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key) - out[key] = struct{}{} - } - return out, rows.Err() - } - codes := setToSortedSlice(codeSet) mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes) - dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet)) dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set)) mmitemIDs := make([]int64, 0, len(mmitemByCode)) for _, c := range codes { @@ -662,7 +604,12 @@ GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 E mmitemIDs = append(mmitemIDs, id) } } - combos, _ := loadProductDimCombos(ctx, pg, mmitemIDs) + colorTokens := setToSortedSlice(colorSet) + for i := range colorTokens { + colorTokens[i] = strings.ToUpper(strings.TrimSpace(colorTokens[i])) + } + dim1ByMmitemToken, _ := loadDim1ByMmitemAndToken(ctx, pg, mmitemIDs, colorTokens) + combos, _ := loadProductDimCombosFromMmitemDim(ctx, pg, mmitemIDs) // Per-request cache for per-mmitem dimval3 inference (dfblob scan). inferCache := map[string]int64{} @@ -728,7 +675,7 @@ LIMIT 1 continue } row.MmitemID = mmitemByCode[row.ProductCode] - row.Dim1ID = dim1ByToken[row.ColorCode] + row.Dim1ID = dim1ByMmitemToken[fmt.Sprintf("%d|%s", row.MmitemID, strings.ToUpper(strings.TrimSpace(row.ColorCode)))] if row.Dim3Code != "" { if v := dim3ByToken[row.Dim3Code]; v > 0 { row.Dim3ID = v @@ -750,7 +697,7 @@ LIMIT 1 case row.MmitemID <= 0: row.MappingWarning = "B2B'de urun yok (mmitem)" case row.Dim1ID <= 0: - row.MappingWarning = "B2B'de renk token eslesmesi yok (mk_dim_token_map: dimval1)" + row.MappingWarning = "B2B'de renk bu urunde yok (mmitem_dim/dfgrp.code)" case row.Dim3Code != "" && row.Dim3ID <= 0: row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)" case !comboOK: @@ -798,6 +745,68 @@ func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]floa return out } +func loadDim1ByMmitemAndToken(ctx context.Context, pg *sql.DB, mmitemIDs []int64, colorTokens []string) (map[string]int64, error) { + out := map[string]int64{} + if len(mmitemIDs) == 0 || len(colorTokens) == 0 { + return out, nil + } + rows, err := pg.QueryContext(ctx, ` +SELECT md.mmitem_id, UPPER(BTRIM(d.code)) AS code, md.val1 +FROM mmitem_dim md +JOIN dfgrp d ON d.id = md.val1 +WHERE md.mmitem_id = ANY($1) + AND md.is_active = TRUE + AND UPPER(BTRIM(d.code)) = ANY($2) +GROUP BY md.mmitem_id, UPPER(BTRIM(d.code)), md.val1 +`, pq.Array(mmitemIDs), pq.Array(colorTokens)) + if err != nil { + return out, err + } + defer rows.Close() + for rows.Next() { + var mmid, dim1 int64 + var code string + if err := rows.Scan(&mmid, &code, &dim1); err != nil { + return out, err + } + key := fmt.Sprintf("%d|%s", mmid, strings.TrimSpace(code)) + if _, ok := out[key]; !ok && dim1 > 0 { + out[key] = dim1 + } + } + return out, rows.Err() +} + +func loadProductDimCombosFromMmitemDim(ctx context.Context, pg *sql.DB, mmitemIDs []int64) (map[string]struct{}, error) { + out := map[string]struct{}{} + if len(mmitemIDs) == 0 { + return out, nil + } + rows, err := pg.QueryContext(ctx, ` +SELECT + mmitem_id, + val1 AS dim1, + CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END AS dim3_key +FROM mmitem_dim +WHERE mmitem_id = ANY($1) + AND is_active = TRUE + AND val1 IS NOT NULL +GROUP BY mmitem_id, val1, CASE WHEN val3 IS NOT NULL AND val3 > 1000 THEN val3 ELSE 0 END +`, pq.Array(mmitemIDs)) + if err != nil { + return out, err + } + defer rows.Close() + for rows.Next() { + var mmitemID, dim1, dim3Key int64 + if err := rows.Scan(&mmitemID, &dim1, &dim3Key); err != nil { + return out, err + } + out[fmt.Sprintf("%d|%d|%d", mmitemID, dim1, dim3Key)] = struct{}{} + } + return out, rows.Err() +} + type saveProductSeriesMappingsRequest struct { Items []struct { ProductCode string `json:"product_code"` @@ -855,10 +864,27 @@ func PostProductSeriesMappingsSaveHandler(pg *sql.DB) http.HandlerFunc { http.Error(w, "PG urun bulunamadi: "+code, http.StatusBadRequest) return } - dim1ID, err := resolveDimTokenIDTx(ctx, tx, "dimval1", color) - if err != nil || dim1ID <= 0 { - http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest) - return + // Authoritative dim1 resolver: only allow saving against a color that exists for this product in mmitem_dim. + var dim1ID int64 + { + tok := strings.ToUpper(strings.TrimSpace(color)) + if tok == "" || tok == "0" { + http.Error(w, "Renk token eslesmesi bulunamadi: "+color, http.StatusBadRequest) + return + } + if err := tx.QueryRowContext(ctx, ` +SELECT md.val1 +FROM mmitem_dim md +JOIN dfgrp d ON d.id = md.val1 +WHERE md.mmitem_id = $1 + AND md.is_active = TRUE + AND UPPER(BTRIM(d.code)) = $2 +GROUP BY md.val1 +LIMIT 1 +`, mmitemID, tok).Scan(&dim1ID); err != nil || dim1ID <= 0 { + http.Error(w, "Renk bu urunde yok: "+color, http.StatusBadRequest) + return + } } var dim3ID sql.NullInt64 if dim3Token != "" {