Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-24 22:54:32 +03:00
parent be7ccdc466
commit 583af0230a
6 changed files with 337 additions and 150 deletions

View File

@@ -292,86 +292,91 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
defByID[d.ID] = d
}
// Strict token->dim_id resolver (no inference/persistence). UI should not invent dim mappings.
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(tokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT token, dim_id
FROM mk_dim_token_map
WHERE dim_column=$1 AND token = ANY($2)
`, column, pq.Array(tokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var tok string
var id int64
if err := rows.Scan(&tok, &id); err != nil {
return out, err
}
out[strings.TrimSpace(tok)] = id
}
return out, rows.Err()
}
// Load all known (product_code, dim1, dim3_key) combos for products in this response.
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(productCodes) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT product_code, dim1, dim3_key
FROM mk_mmitem_dim_combo
WHERE product_code = ANY($1)
`, pq.Array(productCodes))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var code string
var dim1, dim3Key int64
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
return out, err
}
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), dim1, dim3Key)
out[key] = struct{}{}
}
return out, rows.Err()
}
codes := setToSortedSlice(codeSet)
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet))
dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
combos, _ := loadProductDimCombos(ctx, pg, codes)
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
inferCache := map[string]int64{}
inferDim3ForMmitem := func(mmitemID int64, token string) int64 {
mmitemID = int64(mmitemID)
tok := strings.ToUpper(normalizeDimParam(token))
if mmitemID <= 0 || tok == "" {
return 0
}
key := fmt.Sprintf("%d|%s", mmitemID, tok)
if v, ok := inferCache[key]; ok {
return v
}
// Use the same pattern approach as resolveDimvalFromFileNameToken, but scoped to src_id.
patterns := buildNameLikePatterns(tok)
if len(patterns) == 0 {
inferCache[key] = 0
return 0
}
var dimv string
err := pg.QueryRowContext(ctx, `
SELECT x.dimv
FROM (
SELECT COALESCE(dimval3::text, '') AS dimv, COUNT(*) AS cnt
FROM dfblob
WHERE src_table='mmitem'
AND typ='img'
AND src_id=$7
AND COALESCE(dimval3::text, '') <> ''
AND (
UPPER(COALESCE(file_name,'')) LIKE $1 OR
UPPER(COALESCE(file_name,'')) LIKE $2 OR
UPPER(COALESCE(file_name,'')) LIKE $3 OR
UPPER(COALESCE(file_name,'')) LIKE $4 OR
UPPER(COALESCE(file_name,'')) LIKE $5 OR
UPPER(COALESCE(file_name,'')) LIKE $6
)
GROUP BY COALESCE(dimval3::text, '')
) x
ORDER BY x.cnt DESC, x.dimv
LIMIT 1
`, patterns[0], patterns[1], patterns[2], patterns[3], patterns[4], patterns[5], mmitemID).Scan(&dimv)
if err != nil {
inferCache[key] = 0
return 0
}
dimv = strings.TrimSpace(dimv)
if dimv == "" || dimv == "0" {
inferCache[key] = 0
return 0
}
id, err := strconv.ParseInt(dimv, 10, 64)
if err != nil || id <= 0 {
inferCache[key] = 0
return 0
}
inferCache[key] = id
return id
}
out := make([]productSeriesMappingRow, 0, len(grouped))
for _, row := range grouped {
row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByToken[row.ColorCode]
if row.Dim3Code != "" {
// dimval3 can be ambiguous, but if mk_dim_token_map has a row we treat it as source of truth.
// Strict: only accept explicit token map rows for dimval3.
if v := dim3ByToken[row.Dim3Code]; v > 0 {
row.Dim3ID = v
} else if inferred := inferDim3ForMmitem(row.MmitemID, row.Dim3Code); inferred > 0 {
row.Dim3ID = inferred
}
}
// Only show variants that are also known/valid on the PG side (product_code + dim1 + dim3_key).
// This prevents UI from listing stock variants that don't exist in PG's authoritative item-dim combos.
row.MappingReady = row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
dim3Key := int64(0)
if row.Dim3Code != "" {
dim3Key = row.Dim3ID
}
if row.MappingReady {
_, ok := combos[fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)]
row.MappingReady = ok
}
if !row.MappingReady {
row.MappingWarning = "PG urun veya varyant token eslesmesi bulunamadi"
// Skip: do not surface "orphan" variants in the UI.
continue
}
assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID)
appendSeries := func(key string) {
@@ -383,11 +388,6 @@ LIMIT 1
}
}
appendSeries(assignKey)
// Fallback: if we couldn't match dim3-specific assignments (dim3 token ambiguity / missing),
// show the default (dim3 NULL) assignments for the same product+color.
if len(row.SeriesIDs) == 0 && row.Dim3Code != "" && row.Dim1ID > 0 {
appendSeries(assignmentKey(row.ProductCode, row.Dim1ID, 0))
}
sort.Slice(row.Series, func(i, j int) bool { return row.Series[i].Code < row.Series[j].Code })
sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] })
out = append(out, *row)
@@ -416,6 +416,239 @@ LIMIT 1
}
}
func GetProductSeriesOrphanMappingsHandler(pg *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
f := readStockAttrFilters(r)
limit := 0
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n >= 0 {
limit = n
}
}
search := firstNonEmpty(r.URL.Query().Get("q"), r.URL.Query().Get("code"), r.URL.Query().Get("product_code"))
ctx, cancel := context.WithTimeout(r.Context(), 120*time.Second)
defer cancel()
msRows, err := db.MssqlDB.QueryContext(ctx, queries.GetProductSeriesStockRowsQuery,
f.kategori,
f.urunAnaGrubu,
joinFilterValues(f.urunAltGrubu),
joinFilterValues(f.renk),
joinFilterValues(f.renk2),
joinFilterValues(f.urunIcerigi),
joinFilterValues(f.fit),
joinFilterValues(f.drop),
joinFilterValues(f.beden),
strconv.Itoa(limit),
search,
)
if err != nil {
http.Error(w, "Stok seri listesi alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defer msRows.Close()
grouped := map[string]*productSeriesMappingRow{}
codeSet := map[string]struct{}{}
colorSet := map[string]struct{}{}
dim3Set := map[string]struct{}{}
sizeSet := map[string]struct{}{}
for msRows.Next() {
var raw productSeriesStockRawRow
if err := msRows.Scan(
&raw.ProductCode,
&raw.ProductDescription,
&raw.ColorCode,
&raw.ColorTitle,
&raw.Dim3Code,
&raw.SizeCode,
&raw.Qty,
&raw.UrunAnaGrubu,
&raw.UrunAltGrubu,
&raw.Marka,
&raw.DropVal,
&raw.Fit,
&raw.UrunIcerigi,
&raw.Kategori,
); err != nil {
http.Error(w, "Stok satiri okunamadi: "+err.Error(), http.StatusInternalServerError)
return
}
raw.ProductCode = strings.TrimSpace(raw.ProductCode)
raw.ColorCode = strings.TrimSpace(raw.ColorCode)
raw.Dim3Code = strings.TrimSpace(raw.Dim3Code)
raw.SizeCode = strings.TrimSpace(raw.SizeCode)
key := productSeriesKey(raw.ProductCode, raw.ColorCode, raw.Dim3Code)
row := grouped[key]
if row == nil {
row = &productSeriesMappingRow{
RowKey: key,
ProductCode: raw.ProductCode,
ProductDescription: strings.TrimSpace(raw.ProductDescription),
ColorCode: raw.ColorCode,
ColorTitle: strings.TrimSpace(raw.ColorTitle),
Dim3Code: raw.Dim3Code,
UrunAnaGrubu: strings.TrimSpace(raw.UrunAnaGrubu),
UrunAltGrubu: strings.TrimSpace(raw.UrunAltGrubu),
Marka: strings.TrimSpace(raw.Marka),
DropVal: strings.TrimSpace(raw.DropVal),
Fit: strings.TrimSpace(raw.Fit),
UrunIcerigi: strings.TrimSpace(raw.UrunIcerigi),
Kategori: strings.TrimSpace(raw.Kategori),
SizeQty: map[string]float64{},
SeriesIDs: []int64{},
Series: []productSeriesDefinition{},
}
grouped[key] = row
codeSet[raw.ProductCode] = struct{}{}
colorSet[raw.ColorCode] = struct{}{}
if raw.Dim3Code != "" {
dim3Set[raw.Dim3Code] = struct{}{}
}
}
if raw.SizeCode != "" {
row.SizeQty[raw.SizeCode] = raw.Qty
sizeSet[raw.SizeCode] = struct{}{}
}
row.TotalQty += raw.Qty
}
if err := msRows.Err(); err != nil {
http.Error(w, "Stok satirlari okunamadi: "+err.Error(), http.StatusInternalServerError)
return
}
defs, err := listProductSeriesDefinitions(ctx, pg, true)
if err != nil {
http.Error(w, "Seri tanimlari alinamadi: "+err.Error(), http.StatusInternalServerError)
return
}
// Strict token map lookups (no inference).
loadDimTokenIDsStrict := func(ctx context.Context, pg *sql.DB, column string, tokens []string) (map[string]int64, error) {
out := map[string]int64{}
if len(tokens) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT token, dim_id
FROM mk_dim_token_map
WHERE dim_column=$1 AND token = ANY($2)
`, column, pq.Array(tokens))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var tok string
var id int64
if err := rows.Scan(&tok, &id); err != nil {
return out, err
}
out[strings.TrimSpace(tok)] = id
}
return out, rows.Err()
}
loadProductDimCombos := func(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]struct{}, error) {
out := map[string]struct{}{}
if len(productCodes) == 0 {
return out, nil
}
rows, err := pg.QueryContext(ctx, `
SELECT product_code, dim1, dim3_key
FROM mk_mmitem_dim_combo
WHERE product_code = ANY($1)
`, pq.Array(productCodes))
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var code string
var dim1, dim3Key int64
if err := rows.Scan(&code, &dim1, &dim3Key); err != nil {
return out, err
}
key := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(code), 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))
combos, _ := loadProductDimCombos(ctx, pg, codes)
out := make([]productSeriesMappingRow, 0, len(grouped))
for _, row := range grouped {
if row.TotalQty <= 0 {
continue
}
row.MmitemID = mmitemByCode[row.ProductCode]
row.Dim1ID = dim1ByToken[row.ColorCode]
if row.Dim3Code != "" {
if v := dim3ByToken[row.Dim3Code]; v > 0 {
row.Dim3ID = v
}
}
// Determine orphan reason.
baseReady := row.MmitemID > 0 && row.Dim1ID > 0 && (row.Dim3Code == "" || row.Dim3ID > 0)
dim3Key := int64(0)
if row.Dim3Code != "" {
dim3Key = row.Dim3ID
}
comboKey := fmt.Sprintf("%s|%d|%d", strings.TrimSpace(row.ProductCode), row.Dim1ID, dim3Key)
_, comboOK := combos[comboKey]
row.MappingReady = false
switch {
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)"
case row.Dim3Code != "" && row.Dim3ID <= 0:
row.MappingWarning = "B2B'de dim3 token eslesmesi yok (mk_dim_token_map: dimval3)"
case !comboOK:
row.MappingWarning = "B2B'de varyant kombosu yok (mk_mmitem_dim_combo)"
default:
// Not an orphan; skip.
if baseReady && comboOK {
continue
}
row.MappingWarning = "B2B varyant eslesmesi bulunamadi"
}
out = append(out, *row)
}
productTotals := productSeriesTotalQtyByCode(out)
sort.Slice(out, func(i, j int) bool {
totalI := productTotals[out[i].ProductCode]
totalJ := productTotals[out[j].ProductCode]
if totalI != totalJ {
return totalI > totalJ
}
if out[i].ProductCode != out[j].ProductCode {
return out[i].ProductCode < out[j].ProductCode
}
if out[i].ColorCode != out[j].ColorCode {
return out[i].ColorCode < out[j].ColorCode
}
return out[i].Dim3Code < out[j].Dim3Code
})
writeJSON(w, productSeriesMappingsResponse{
Rows: out,
SizeColumns: sortSizeColumns(setToSortedSlice(sizeSet)),
Definitions: defs,
})
}
}
func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 {
out := make(map[string]float64, len(rows))
for _, row := range rows {