Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -950,6 +950,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
|
|||||||
"pricing", "view",
|
"pricing", "view",
|
||||||
wrapV3(http.HandlerFunc(routes.GetProductSeriesMappingsHandler(pgDB))),
|
wrapV3(http.HandlerFunc(routes.GetProductSeriesMappingsHandler(pgDB))),
|
||||||
)
|
)
|
||||||
|
bindV3(r, pgDB,
|
||||||
|
"/api/pricing/product-series/mappings/orphans", "GET",
|
||||||
|
"pricing", "view",
|
||||||
|
wrapV3(http.HandlerFunc(routes.GetProductSeriesOrphanMappingsHandler(pgDB))),
|
||||||
|
)
|
||||||
bindV3(r, pgDB,
|
bindV3(r, pgDB,
|
||||||
"/api/pricing/product-series/mappings/save", "POST",
|
"/api/pricing/product-series/mappings/save", "POST",
|
||||||
"pricing", "update",
|
"pricing", "update",
|
||||||
|
|||||||
@@ -1167,6 +1167,25 @@ func productSeriesResolvePGVariant(ctx context.Context, pg *sql.DB, productCode,
|
|||||||
}
|
}
|
||||||
dim3ID = sql.NullInt64{Int64: id, Valid: true}
|
dim3ID = sql.NullInt64{Int64: id, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only treat variants as ready if they exist in authoritative item-dim combos.
|
||||||
|
// This prevents writing series assignments for stock variants that don't exist on the PG side.
|
||||||
|
dim3Key := int64(0)
|
||||||
|
if dim3ID.Valid {
|
||||||
|
dim3Key = dim3ID.Int64
|
||||||
|
}
|
||||||
|
var exists int
|
||||||
|
if err := pg.QueryRowContext(ctx, `
|
||||||
|
SELECT 1
|
||||||
|
FROM mk_mmitem_dim_combo
|
||||||
|
WHERE product_code=$1 AND dim1=$2 AND dim3_key=$3
|
||||||
|
LIMIT 1
|
||||||
|
`, strings.TrimSpace(productCode), dim1ID, dim3Key).Scan(&exists); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return 0, 0, sql.NullInt64{}, false, nil
|
||||||
|
}
|
||||||
|
return 0, 0, sql.NullInt64{}, false, err
|
||||||
|
}
|
||||||
return mmitemID, dim1ID, dim3ID, true, nil
|
return mmitemID, dim1ID, dim3ID, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,12 +133,6 @@ CREATE TABLE IF NOT EXISTS mk_dim_token_map (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time bootstrap for mk_dim_token_map from authoritative PG data, if empty.
|
|
||||||
// This avoids heuristics (dfblob filename inference) polluting the token map on first deploy.
|
|
||||||
if err := seedDimTokenMapFromMmitemDim(pg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := seedPricingTargetMapRows(pg, "mk_price_target_map_pg", "sdprcgrp_id"); err != nil {
|
if err := seedPricingTargetMapRows(pg, "mk_price_target_map_pg", "sdprcgrp_id"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -161,81 +155,6 @@ WHERE is_active = TRUE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedDimTokenMapFromMmitemDim(pg *sql.DB) error {
|
|
||||||
// Seed dimval1 only if it's currently empty.
|
|
||||||
var hasDimval1 int
|
|
||||||
_ = pg.QueryRow(`SELECT 1 FROM mk_dim_token_map WHERE dim_column='dimval1' LIMIT 1`).Scan(&hasDimval1)
|
|
||||||
if hasDimval1 == 0 {
|
|
||||||
// Prefer the most-used dim_id for a given code if duplicates exist, to keep the mapping stable.
|
|
||||||
if _, err := pg.Exec(`
|
|
||||||
WITH usage AS (
|
|
||||||
SELECT val1 AS dim_id, COUNT(*) AS cnt
|
|
||||||
FROM mmitem_dim
|
|
||||||
WHERE val1 IS NOT NULL
|
|
||||||
GROUP BY val1
|
|
||||||
),
|
|
||||||
candidates AS (
|
|
||||||
SELECT UPPER(BTRIM(d.code)) AS token, d.id AS dim_id, u.cnt
|
|
||||||
FROM usage u
|
|
||||||
JOIN dfgrp d ON d.id = u.dim_id
|
|
||||||
WHERE d.code IS NOT NULL
|
|
||||||
AND BTRIM(d.code) <> ''
|
|
||||||
),
|
|
||||||
picked AS (
|
|
||||||
SELECT DISTINCT ON (token) token, dim_id
|
|
||||||
FROM candidates
|
|
||||||
ORDER BY token, cnt DESC, dim_id
|
|
||||||
)
|
|
||||||
INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at)
|
|
||||||
SELECT 'dimval1', token, dim_id, now()
|
|
||||||
FROM picked
|
|
||||||
ON CONFLICT (dim_column, token)
|
|
||||||
DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|
||||||
`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed dimval3 only if it's currently empty and there is at least one val3 in mmitem_dim.
|
|
||||||
var hasDimval3 int
|
|
||||||
_ = pg.QueryRow(`SELECT 1 FROM mk_dim_token_map WHERE dim_column='dimval3' LIMIT 1`).Scan(&hasDimval3)
|
|
||||||
if hasDimval3 == 0 {
|
|
||||||
var anyVal3 int
|
|
||||||
_ = pg.QueryRow(`SELECT 1 FROM mmitem_dim WHERE val3 IS NOT NULL LIMIT 1`).Scan(&anyVal3)
|
|
||||||
if anyVal3 != 0 {
|
|
||||||
if _, err := pg.Exec(`
|
|
||||||
WITH usage AS (
|
|
||||||
SELECT val3 AS dim_id, COUNT(*) AS cnt
|
|
||||||
FROM mmitem_dim
|
|
||||||
WHERE val3 IS NOT NULL
|
|
||||||
GROUP BY val3
|
|
||||||
),
|
|
||||||
candidates AS (
|
|
||||||
SELECT UPPER(BTRIM(d.code)) AS token, d.id AS dim_id, u.cnt
|
|
||||||
FROM usage u
|
|
||||||
JOIN dfgrp d ON d.id = u.dim_id
|
|
||||||
WHERE d.code IS NOT NULL
|
|
||||||
AND BTRIM(d.code) <> ''
|
|
||||||
),
|
|
||||||
picked AS (
|
|
||||||
SELECT DISTINCT ON (token) token, dim_id
|
|
||||||
FROM candidates
|
|
||||||
ORDER BY token, cnt DESC, dim_id
|
|
||||||
)
|
|
||||||
INSERT INTO mk_dim_token_map (dim_column, token, dim_id, updated_at)
|
|
||||||
SELECT 'dimval3', token, dim_id, now()
|
|
||||||
FROM picked
|
|
||||||
ON CONFLICT (dim_column, token)
|
|
||||||
DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
|
|
||||||
`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedPricingTargetMapRows(pg *sql.DB, tableName string, valueColumn string) error {
|
func seedPricingTargetMapRows(pg *sql.DB, tableName string, valueColumn string) error {
|
||||||
currencies := []string{"TRY", "USD", "EUR"}
|
currencies := []string{"TRY", "USD", "EUR"}
|
||||||
for _, currency := range currencies {
|
for _, currency := range currencies {
|
||||||
|
|||||||
@@ -292,86 +292,91 @@ func GetProductSeriesMappingsHandler(pg *sql.DB) http.HandlerFunc {
|
|||||||
defByID[d.ID] = d
|
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)
|
codes := setToSortedSlice(codeSet)
|
||||||
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
|
mmitemByCode, _ := loadMmitemIDs(ctx, pg, codes)
|
||||||
dim1ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
dim1ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval1", setToSortedSlice(colorSet))
|
||||||
dim3ByToken, _ := loadDimTokenIDs(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
dim3ByToken, _ := loadDimTokenIDsStrict(ctx, pg, "dimval3", setToSortedSlice(dim3Set))
|
||||||
|
combos, _ := loadProductDimCombos(ctx, pg, codes)
|
||||||
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
|
existing, _ := loadProductSeriesAssignments(ctx, pg, codes)
|
||||||
|
|
||||||
// Per-request cache for per-mmitem dimval3 inference to avoid repeated dfblob scans.
|
// 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))
|
out := make([]productSeriesMappingRow, 0, len(grouped))
|
||||||
for _, row := range grouped {
|
for _, row := range grouped {
|
||||||
row.MmitemID = mmitemByCode[row.ProductCode]
|
row.MmitemID = mmitemByCode[row.ProductCode]
|
||||||
row.Dim1ID = dim1ByToken[row.ColorCode]
|
row.Dim1ID = dim1ByToken[row.ColorCode]
|
||||||
if row.Dim3Code != "" {
|
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 {
|
if v := dim3ByToken[row.Dim3Code]; v > 0 {
|
||||||
row.Dim3ID = v
|
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)
|
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 {
|
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)
|
assignKey := assignmentKey(row.ProductCode, row.Dim1ID, row.Dim3ID)
|
||||||
appendSeries := func(key string) {
|
appendSeries := func(key string) {
|
||||||
@@ -383,11 +388,6 @@ LIMIT 1
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
appendSeries(assignKey)
|
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.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] })
|
sort.Slice(row.SeriesIDs, func(i, j int) bool { return row.SeriesIDs[i] < row.SeriesIDs[j] })
|
||||||
out = append(out, *row)
|
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 {
|
func productSeriesTotalQtyByCode(rows []productSeriesMappingRow) map[string]float64 {
|
||||||
out := make(map[string]float64, len(rows))
|
out := make(map[string]float64, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
|
|||||||
@@ -373,6 +373,11 @@ const menuItems = [
|
|||||||
to: '/app/pricing/product-series-mappings',
|
to: '/app/pricing/product-series-mappings',
|
||||||
permission: 'pricing:view'
|
permission: 'pricing:view'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'B2B Olmayan Stok',
|
||||||
|
to: '/app/pricing/product-series-mappings-orphans',
|
||||||
|
permission: 'pricing:view'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Ürün Seri Tanımlamaları',
|
label: 'Ürün Seri Tanımlamaları',
|
||||||
to: '/app/pricing/product-series-definitions',
|
to: '/app/pricing/product-series-definitions',
|
||||||
|
|||||||
@@ -425,6 +425,12 @@ const routes = [
|
|||||||
component: () => import('pages/ProductSeriesMappings.vue'),
|
component: () => import('pages/ProductSeriesMappings.vue'),
|
||||||
meta: { permission: 'pricing:view' }
|
meta: { permission: 'pricing:view' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'pricing/product-series-mappings-orphans',
|
||||||
|
name: 'product-series-mappings-orphans',
|
||||||
|
component: () => import('pages/ProductSeriesMappingsOrphans.vue'),
|
||||||
|
meta: { permission: 'pricing:view' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'pricing/product-series-definitions',
|
path: 'pricing/product-series-definitions',
|
||||||
name: 'product-series-definitions',
|
name: 'product-series-definitions',
|
||||||
|
|||||||
Reference in New Issue
Block a user