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

@@ -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 {
return nil, err
}
// Resolve to PG dim ids (e-commerce expects integer ids, e.g. dim1=82, dim3=2182).
// Nebim varies by installation; we try a few fallbacks. In most setups:
// - dim1 is size (ItemDim1Code)
// - dim3 is color (ColorCode)
// Resolve to PG dim ids. For this installation we align with PG's authoritative mmitem_dim model:
// - dim1 corresponds to mmitem_dim.val1 (typically Color).
// - dim3 corresponds to mmitem_dim.val2 (typically Size).
d1 := int64(0)
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
d1 = id
resolvedDim1++
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
d1 = id
resolvedDim1++
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok {
} else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
d1 = id
resolvedDim1++
}
@@ -610,12 +609,11 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
continue
}
var d3 sql.NullInt64
// IMPORTANT: In this Nebim setup, both ItemDim1Code and ItemDim3Code are mapped in PG as dimval1 ids.
// We therefore resolve dim3 via dimval1 as well (not dimval3).
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim3Code); ok {
// dim3 corresponds to mmitem_dim.val2 (usually ItemDim1Code).
if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); ok {
d3 = sql.NullInt64{Int64: id, Valid: true}
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}
resolvedDim3++
}
@@ -645,6 +643,119 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at
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 {
productCode = strings.TrimSpace(productCode)
if productCode == "" || len(dims) == 0 {
@@ -983,35 +1094,45 @@ VALUES (
mmItemID = 0
}
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 {
cacheStarted := time.Now()
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(),
)
// 1) Authoritative source: mmitem_dim (PG).
if d, err := loadDimsFromPgMMItemDim(mmItemID, code); err == nil && len(d) > 0 {
dims = d
_ = 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).
if len(dims) == 0 {
d, err := loadDimsFromMssqlStock(code)
if err != nil {
logger.Error("save:pg:dims:mssql:error", "product_code", code, "err", err)
} else {
dims = d
if err := upsertDimCombosCache(code, dims); err != nil {
logger.Error("save:pg:dims:cache:error", "product_code", code, "err", err)
}
_ = upsertDimCombosCache(code, dims)
// If PG doesn't have mmitem_dim rows for this product yet, try to seed them.
ensureMMItemDimRows(mmItemID, dims, nil)
}
}
}