Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-06-18 15:59:24 +03:00
parent 21b1242a5a
commit d1f1e5a4f4
3 changed files with 375 additions and 89 deletions

View File

@@ -258,16 +258,40 @@ norm AS (
COALESCE(price, 0) AS price COALESCE(price, 0) AS price
FROM input FROM input
), ),
dims_cache AS ( -- Prefer PG's authoritative variant dimension table (mmitem_dim). Fall back to cache table if needed.
dims_mmitem_dim AS (
SELECT
norm.product_code AS product_code,
md.val1::bigint AS dim1,
CASE
WHEN md.val2 IS NULL OR md.val2 = 0 THEN NULL
ELSE md.val2::bigint
END AS dim3
FROM norm
JOIN mmitem mm
ON mm.code = norm.product_code
JOIN mmitem_dim md
ON md.mmitem_id = mm.id
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 SELECT
NULLIF(BTRIM(c.product_code), '') AS product_code, NULLIF(BTRIM(c.product_code), '') AS product_code,
c.dim1, c.dim1::bigint AS dim1,
c.dim3 c.dim3::bigint AS dim3
FROM mk_mmitem_dim_combo c FROM mk_mmitem_dim_combo c
JOIN norm JOIN norm
ON norm.product_code = c.product_code ON norm.product_code = c.product_code
WHERE c.dim1 IS NOT NULL 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 ( dims_sdprc AS (
SELECT SELECT
norm.product_code AS product_code, norm.product_code AS product_code,

View File

@@ -591,18 +591,17 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
if err := rows.Scan(&colorCode, &dim1Code, &dim3Code); err != nil { if err := rows.Scan(&colorCode, &dim1Code, &dim3Code); err != nil {
return nil, err return nil, err
} }
// Resolve to PG dim ids (e-commerce expects integer ids, e.g. dim1=82, dim3=2182). // Resolve to PG dim ids. For this installation we align with PG's authoritative mmitem_dim model:
// Nebim varies by installation; we try a few fallbacks. In most setups: // - dim1 corresponds to mmitem_dim.val1 (typically Color).
// - dim1 is size (ItemDim1Code) // - dim3 corresponds to mmitem_dim.val2 (typically Size).
// - dim3 is color (ColorCode)
d1 := int64(0) d1 := int64(0)
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok { if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
d1 = id d1 = id
resolvedDim1++ resolvedDim1++
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok { } else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
d1 = id d1 = id
resolvedDim1++ resolvedDim1++
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok { } else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
d1 = id d1 = id
resolvedDim1++ resolvedDim1++
} }
@@ -610,12 +609,11 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
continue continue
} }
var d3 sql.NullInt64 var d3 sql.NullInt64
// IMPORTANT: In this Nebim setup, both ItemDim1Code and ItemDim3Code are mapped in PG as dimval1 ids. // dim3 corresponds to mmitem_dim.val2 (usually ItemDim1Code).
// We therefore resolve dim3 via dimval1 as well (not dimval3). if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
d3 = sql.NullInt64{Int64: id, Valid: true} d3 = sql.NullInt64{Int64: id, Valid: true}
resolvedDim3++ resolvedDim3++
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok { } else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
d3 = sql.NullInt64{Int64: id, Valid: true} d3 = sql.NullInt64{Int64: id, Valid: true}
resolvedDim3++ resolvedDim3++
} }
@@ -645,6 +643,119 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
return out, nil return out, nil
} }
ensureMMItemDimRows := func(mmItemID int64, combos []dimCombo, extraVal3 map[string]int64) {
if mmItemID <= 0 || len(combos) == 0 {
return
}
// Best-effort: don't assume a specific unique constraint exists on mmitem_dim.
for _, c := range combos {
if c.Dim1 <= 0 {
continue
}
v2 := int64(0)
var v2any any = nil
if c.Dim3.Valid && c.Dim3.Int64 > 0 {
v2 = c.Dim3.Int64
v2any = v2
}
// If we managed to resolve an "ItemDim3Code" id too, store it in val3 and mark mmdim_id=3.
v3 := int64(0)
if extraVal3 != nil {
if vv, ok := extraVal3[fmt.Sprintf("%d|%d", c.Dim1, v2)]; ok && vv > 0 {
v3 = vv
}
}
mmdimID := int64(2)
var v3any any = nil
if v3 > 0 {
mmdimID = 3
v3any = v3
}
_, _ = pgTx.ExecContext(ctx, `
INSERT INTO mmitem_dim (mmitem_id, mmdim_id, val1, val2, val3, is_active, qty)
SELECT $1, $2, $3, $4, $5, TRUE, 0
WHERE NOT EXISTS (
SELECT 1
FROM mmitem_dim
WHERE mmitem_id = $1
AND mmdim_id = $2
AND val1 = $3
AND COALESCE(val2, 0) = COALESCE($4::bigint, 0)
AND COALESCE(val3, 0) = COALESCE($5::bigint, 0)
LIMIT 1
);
`, mmItemID, mmdimID, c.Dim1, v2any, v3any)
}
}
loadDimsFromPgMMItemDim := func(mmItemID int64, productCode string) ([]dimCombo, error) {
started := time.Now()
if mmItemID <= 0 {
return nil, fmt.Errorf("invalid mmitem_id")
}
rows, err := pgTx.QueryContext(ctx, `
SELECT mmdim_id, val1, val2, val3
FROM mmitem_dim
WHERE mmitem_id = $1
AND COALESCE(is_active, TRUE) = TRUE
`, mmItemID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]dimCombo, 0, 64)
seen := make(map[string]struct{}, 128)
readRows := 0
for rows.Next() {
readRows++
var mmdimID sql.NullInt64
var v1 sql.NullInt64
var v2 sql.NullInt64
var v3 sql.NullInt64
if err := rows.Scan(&mmdimID, &v1, &v2, &v3); err != nil {
return nil, err
}
if !v1.Valid || v1.Int64 <= 0 {
continue
}
d1 := v1.Int64
var d3 sql.NullInt64
// In production data we've observed the variant key as (val1, val2). val3 may exist but is not
// used as the second axis for pricing/campaign matching in this app.
_ = mmdimID
_ = v3
if v2.Valid && v2.Int64 > 0 {
d3 = sql.NullInt64{Int64: v2.Int64, Valid: true}
}
key := fmt.Sprintf("%d|%d", d1, func() int64 {
if d3.Valid {
return d3.Int64
}
return 0
}())
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, dimCombo{Dim1: d1, Dim3: d3})
}
if err := rows.Err(); err != nil {
return nil, err
}
logger.Info("save:pg:dims:mmitem-dim:loaded",
"product_code", strings.TrimSpace(productCode),
"mmitem_id", mmItemID,
"rows_read", readRows,
"dims", len(out),
"duration_ms", time.Since(started).Milliseconds(),
)
return out, nil
}
upsertDimCombosCache := func(productCode string, dims []dimCombo) error { upsertDimCombosCache := func(productCode string, dims []dimCombo) error {
productCode = strings.TrimSpace(productCode) productCode = strings.TrimSpace(productCode)
if productCode == "" || len(dims) == 0 { if productCode == "" || len(dims) == 0 {
@@ -983,35 +1094,45 @@ VALUES (
mmItemID = 0 mmItemID = 0
} }
dims := []dimCombo{} dims := []dimCombo{}
// Prefer cached dim combos (fast). If not present, load from Nebim stock query (used by product-stock-query UI). // Prefer PG's own authoritative dim combo table (mmitem_dim). Cache is only a fast-path fallback.
if mmItemID > 0 { if mmItemID > 0 {
cacheStarted := time.Now() // 1) Authoritative source: mmitem_dim (PG).
cached, cacheErr := loadDimCombosFromCache(code) if d, err := loadDimsFromPgMMItemDim(mmItemID, code); err == nil && len(d) > 0 {
if cacheErr == nil && len(cached) > 0 { dims = d
dims = cached _ = upsertDimCombosCache(code, dims) // best-effort cache fill
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(),
)
} }
// 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).
if len(dims) == 0 { if len(dims) == 0 {
d, err := loadDimsFromMssqlStock(code) d, err := loadDimsFromMssqlStock(code)
if err != nil { if err != nil {
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err) logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
} else { } else {
dims = d dims = d
if err := upsertDimCombosCache(code, dims); err != nil { _ = upsertDimCombosCache(code, dims)
logger.Error("save:pg:dims:cache:error", "product_code", code, "err", err) // If PG doesn't have mmitem_dim rows for this product yet, try to seed them.
} ensureMMItemDimRows(mmItemID, dims, nil)
} }
} }
} }

View File

@@ -642,6 +642,7 @@ func GetWholesaleCampaignVariantRowsHandler(pg *sql.DB, mssql *sql.DB) http.Hand
// Resolve mmitem ids in bulk. // Resolve mmitem ids in bulk.
codeToItem := make(map[string]int64, len(codes)) codeToItem := make(map[string]int64, len(codes))
itemToCode := make(map[int64]string, len(codes))
{ {
rows, err := pg.QueryContext(ctx, ` rows, err := pg.QueryContext(ctx, `
SELECT code, id SELECT code, id
@@ -663,6 +664,7 @@ WHERE code = ANY($1::text[])
c = strings.TrimSpace(c) c = strings.TrimSpace(c)
if c != "" && id > 0 { if c != "" && id > 0 {
codeToItem[c] = id codeToItem[c] = id
itemToCode[id] = c
} }
} }
rows.Close() rows.Close()
@@ -727,15 +729,6 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
return id, true return id, true
} }
// MSSQL: variant+stock list for selected products.
joined := strings.Join(codes, ",")
msRows, err := mssql.QueryContext(ctx, queries.GetWholesaleCampaignVariantStockByProducts, joined)
if err != nil {
http.Error(w, "variant stock query error: "+err.Error(), http.StatusInternalServerError)
return
}
defer msRows.Close()
type tmpRow struct { type tmpRow struct {
ProductCode string ProductCode string
VariantCode string VariantCode string
@@ -744,8 +737,138 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
Dim1 int64 Dim1 int64
Dim3Key int64 Dim3Key int64
} }
// Deduplicate by (mmitem_id, dim1, dim3_key) and aggregate stock qty.
// Build base variant keys from PG's authoritative table (mmitem_dim).
itemIDs := make([]int64, 0, len(codeToItem))
for _, id := range codeToItem {
itemIDs = append(itemIDs, id)
}
tmpMap := make(map[string]tmpRow, 4096) tmpMap := make(map[string]tmpRow, 4096)
hasMMItemDim := make(map[int64]bool, len(itemIDs))
dimIDs := make([]int64, 0, 8192)
if len(itemIDs) > 0 {
rows, err := pg.QueryContext(ctx, `
SELECT mmitem_id, mmdim_id, val1, val2, val3
FROM mmitem_dim
WHERE mmitem_id = ANY($1::bigint[])
AND COALESCE(is_active, TRUE) = TRUE
`, pq.Array(itemIDs))
if err != nil {
http.Error(w, "mmitem_dim lookup error: "+err.Error(), http.StatusInternalServerError)
return
}
for rows.Next() {
var itemID int64
var mmdimID sql.NullInt64
var v1 sql.NullInt64
var v2 sql.NullInt64
var v3 sql.NullInt64
if err := rows.Scan(&itemID, &mmdimID, &v1, &v2, &v3); err != nil {
rows.Close()
http.Error(w, "mmitem_dim scan error", http.StatusInternalServerError)
return
}
hasMMItemDim[itemID] = true
if !v1.Valid || v1.Int64 <= 0 {
continue
}
d1 := v1.Int64
d3k := int64(0)
// Variant key in this app is (val1, val2). val3 may exist but is not the second axis for matching.
_ = mmdimID
_ = v3
if v2.Valid && v2.Int64 > 0 {
d3k = v2.Int64
}
code := strings.TrimSpace(itemToCode[itemID])
if code == "" {
continue
}
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k)
if _, ok := tmpMap[key]; ok {
continue
}
tmpMap[key] = tmpRow{
ProductCode: code,
VariantCode: "",
StockQty: 0,
ItemID: itemID,
Dim1: d1,
Dim3Key: d3k,
}
dimIDs = append(dimIDs, d1)
if d3k > 0 {
dimIDs = append(dimIDs, d3k)
}
}
rows.Close()
}
// Resolve dim ids -> tokens for a readable VariantCode.
idToToken := map[int64]string{}
if len(dimIDs) > 0 {
// uniq
uniq := make([]int64, 0, len(dimIDs))
seen := make(map[int64]struct{}, len(dimIDs))
for _, id := range dimIDs {
if id <= 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
uniq = append(uniq, id)
}
if len(uniq) > 0 {
rows, err := pg.QueryContext(ctx, `
SELECT DISTINCT ON (dim_id) dim_id, token
FROM mk_dim_token_map
WHERE dim_column = 'dimval1'
AND dim_id = ANY($1::bigint[])
ORDER BY dim_id, updated_at DESC;
`, pq.Array(uniq))
if err == nil {
for rows.Next() {
var id int64
var tok string
_ = rows.Scan(&id, &tok)
tok = strings.TrimSpace(tok)
if tok != "" {
idToToken[id] = tok
}
}
rows.Close()
}
}
}
for k, v := range tmpMap {
t1 := strings.TrimSpace(idToToken[v.Dim1])
if t1 == "" {
t1 = fmt.Sprintf("%d", v.Dim1)
}
if v.Dim3Key > 0 {
t3 := strings.TrimSpace(idToToken[v.Dim3Key])
if t3 == "" {
t3 = fmt.Sprintf("%d", v.Dim3Key)
}
v.VariantCode = t1 + "-" + t3
} else {
v.VariantCode = t1
}
tmpMap[k] = v
}
// MSSQL: stock list for selected products; map to (mmitem_id, dim1, dim3_key) via token->id mapping.
joined := strings.Join(codes, ",")
msRows, err := mssql.QueryContext(ctx, queries.GetWholesaleCampaignVariantStockByProducts, joined)
if err != nil {
http.Error(w, "variant stock query error: "+err.Error(), http.StatusInternalServerError)
return
}
defer msRows.Close()
for msRows.Next() { for msRows.Next() {
var itemCode, colorCode, dim1Code, dim3Code string var itemCode, colorCode, dim1Code, dim3Code string
var qty sql.NullFloat64 var qty sql.NullFloat64
@@ -762,66 +885,84 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
continue continue
} }
// Variant token: prefer ColorCode; ItemDim1Code may represent a different attribute. // Map Nebim tokens to PG integer ids (dimval1 namespace).
t1 := strings.TrimSpace(colorCode) // This app uses key: dim1=<color>, dim3=<size> to match mmitem_dim (val1,val2).
if t1 == "" || t1 == "0" {
t1 = strings.TrimSpace(dim1Code)
}
t3 := strings.TrimSpace(dim3Code)
varCode := strings.TrimSpace(t1)
if varCode != "" && t3 != "" && t3 != "0" {
varCode = varCode + "-" + strings.TrimSpace(t3)
}
if varCode == "" {
continue
}
d1 := int64(0) d1 := int64(0)
// IMPORTANT: In this Nebim setup, both ItemDim1Code and ItemDim3Code are mapped in PG as dimval1 ids. if id, ok := resolveDimID("dimval1", colorCode); ok {
// We therefore resolve both axes via dimval1 and store the second axis in dim3 (still a bigint).
if id, ok := resolveDimID("dimval1", dim1Code); ok {
d1 = id
} else if id, ok := resolveDimID("dimval1", dim3Code); ok {
d1 = id
} else if id, ok := resolveDimID("dimval1", colorCode); ok {
d1 = id d1 = id
} }
if d1 <= 0 { if d1 <= 0 {
continue continue
} }
d3k := int64(0) d3k := int64(0)
if id, ok := resolveDimID("dimval1", dim3Code); ok { if id, ok := resolveDimID("dimval1", dim1Code); ok {
d3k = id
} else if id, ok := resolveDimID("dimval1", colorCode); ok {
d3k = id d3k = id
} }
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k)
prev, ok := tmpMap[key]
if !ok {
// If PG does not have mmitem_dim rows for this item yet, seed it from MSSQL and include it.
if !hasMMItemDim[itemID] {
var v2 any = nil
if d3k > 0 {
v2 = d3k
}
v3 := int64(0)
if id, ok := resolveDimID("dimval1", dim3Code); ok {
v3 = id
}
mmdimID := int64(2)
var v3any any = nil
if v3 > 0 {
mmdimID = 3
v3any = v3
}
_, _ = pg.ExecContext(ctx, `
INSERT INTO mmitem_dim (mmitem_id, mmdim_id, val1, val2, val3, is_active, qty)
SELECT $1, $2, $3, $4, $5, TRUE, 0
WHERE NOT EXISTS (
SELECT 1
FROM mmitem_dim
WHERE mmitem_id = $1
AND mmdim_id = $2
AND val1 = $3
AND COALESCE(val2, 0) = COALESCE($4::bigint, 0)
AND COALESCE(val3, 0) = COALESCE($5::bigint, 0)
LIMIT 1
);
`, itemID, mmdimID, d1, v2, v3any)
hasMMItemDim[itemID] = true
code := strings.TrimSpace(itemToCode[itemID])
if code != "" {
tmpMap[key] = tmpRow{
ProductCode: code,
VariantCode: "",
StockQty: 0,
ItemID: itemID,
Dim1: d1,
Dim3Key: d3k,
}
// Keep dim token cache for VariantCode formatting.
dimIDs = append(dimIDs, d1)
if d3k > 0 {
dimIDs = append(dimIDs, d3k)
}
prev = tmpMap[key]
ok = true
}
}
if !ok {
continue
}
}
q := 0.0 q := 0.0
if qty.Valid { if qty.Valid {
q = qty.Float64 q = qty.Float64
} }
key := fmt.Sprintf("%d|%d|%d", itemID, d1, d3k) prev.StockQty += q
if prev, ok := tmpMap[key]; ok { tmpMap[key] = prev
prev.StockQty += q _ = colorCode // display-only
// Keep the first non-empty variant code.
if prev.VariantCode == "" {
prev.VariantCode = varCode
}
tmpMap[key] = prev
} else {
tmpMap[key] = tmpRow{
ProductCode: itemCode,
VariantCode: varCode,
StockQty: q,
ItemID: itemID,
Dim1: d1,
Dim3Key: d3k,
}
}
}
if err := msRows.Err(); err != nil {
http.Error(w, "variant stock read error: "+err.Error(), http.StatusInternalServerError)
return
} }
tmp := make([]tmpRow, 0, len(tmpMap)) tmp := make([]tmpRow, 0, len(tmpMap))