From 21b1242a5ab10eceafe65a9a90d8997d1bc10ed1 Mon Sep 17 00:00:00 2001 From: M_Kececi Date: Thu, 18 Jun 2026 15:20:34 +0300 Subject: [PATCH] Merge remote-tracking branch 'origin/master' --- svc/routes/product_pricing_save.go | 55 +++++++++++++++++++- svc/routes/wholesale_campaign_mail.go | 73 +++++++++++++++++++++++++-- svc/routes/wholesale_campaigns.go | 13 +++-- 3 files changed, 132 insertions(+), 9 deletions(-) diff --git a/svc/routes/product_pricing_save.go b/svc/routes/product_pricing_save.go index 426ba8a..a29f054 100644 --- a/svc/routes/product_pricing_save.go +++ b/svc/routes/product_pricing_save.go @@ -392,6 +392,45 @@ WHERE is_active = TRUE Price float64 `json:"price"` } + // sdprc has a unique constraint on the business key (tier/currency/dims). + // If input rows contain duplicates for the same key (can happen due to tier/group mapping), + // we must dedupe before bulk inserting to avoid 500s like "duplicate key violates uq_sdprc_2". + dedupeSdprcWriteRows := func(productCode string, in []sdprcWriteRow) []sdprcWriteRow { + if len(in) <= 1 { + return in + } + type key struct { + Cur string + Grp int + D1 int64 + D3 int64 // 0 => NULL + } + idx := make(map[key]int, len(in)) + out := make([]sdprcWriteRow, 0, len(in)) + for _, r := range in { + cur := strings.ToUpper(strings.TrimSpace(r.Currency)) + d3k := int64(0) + if r.Dim3 != nil && *r.Dim3 > 0 { + d3k = *r.Dim3 + } + k := key{Cur: cur, Grp: r.SdprcGrpID, D1: r.Dim1, D3: d3k} + if i, ok := idx[k]; ok { + out[i] = r // keep last + continue + } + idx[k] = len(out) + out = append(out, r) + } + if len(out) != len(in) { + logger.Warn("save:pg:sdprc:dedupe", + "product_code", strings.TrimSpace(productCode), + "in_rows", len(in), + "out_rows", len(out), + ) + } + return out + } + loadDimCombosFromCache := func(productCode string) ([]dimCombo, error) { productCode = strings.TrimSpace(productCode) if productCode == "" { @@ -552,11 +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). + // 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) d1 := int64(0) if id, ok := resolveDimvalFromToken(pgTx, "dimval1", dim1Code); 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 { d1 = id resolvedDim1++ @@ -565,7 +610,12 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at continue } var d3 sql.NullInt64 - if id, ok := resolveDimvalFromToken(pgTx, "dimval3", dim3Code); ok { + // 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 { + d3 = sql.NullInt64{Int64: id, Valid: true} + resolvedDim3++ + } else if id, ok := resolveDimvalFromToken(pgTx, "dimval1", colorCode); ok { d3 = sql.NullInt64{Int64: id, Valid: true} resolvedDim3++ } @@ -1073,6 +1123,7 @@ VALUES ( } } if len(writeRows) > 0 { + writeRows = dedupeSdprcWriteRows(code, writeRows) startPG := time.Now() inserted, err := bulkAppendOnlyInsertSdprc(mmItemID, code, writeRows) if err != nil { diff --git a/svc/routes/wholesale_campaign_mail.go b/svc/routes/wholesale_campaign_mail.go index 4978ed3..8eeaa11 100644 --- a/svc/routes/wholesale_campaign_mail.go +++ b/svc/routes/wholesale_campaign_mail.go @@ -24,6 +24,8 @@ type wholesaleCampaignMailRow struct { BrandGroupSec string Dim1 int64 Dim3 int64 + Dim1Token string + Dim3Token string CampaignCode string CampaignTitle string DiscountRate float64 @@ -73,10 +75,10 @@ func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesal r.BrandGroupSec, r.Marka, r.ProductCode, - fmt.Sprintf("%d", r.Dim1), + strings.TrimSpace(r.Dim1Token), func() string { - if r.Dim3 > 0 { - return fmt.Sprintf("%d", r.Dim3) + if strings.TrimSpace(r.Dim3Token) != "" { + return strings.TrimSpace(r.Dim3Token) } return "" }(), @@ -197,7 +199,52 @@ ORDER BY mm.code, l.dim1, COALESCE(l.dim3, 0), sc.discount_rate DESC; } defer rows.Close() + // Resolve dim ids -> human tokens so mails show Nebim-style variant codes (not numeric ids). + resolveDimTokens := func(ctx context.Context, dimIDs []int64) map[int64]string { + out := make(map[int64]string, len(dimIDs)) + 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 { + return out + } + tRows, 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 { + return out + } + defer tRows.Close() + for tRows.Next() { + var id int64 + var tok string + if err := tRows.Scan(&id, &tok); err == nil { + tok = strings.TrimSpace(tok) + if tok != "" { + out[id] = tok + } + } + } + return out + } + mailRows := make([]wholesaleCampaignMailRow, 0, 1024) + dimIDs := make([]int64, 0, 2048) for rows.Next() { var r dbRow if err := rows.Scan(&r.ProductCode, &r.Dim1, &r.Dim3, &r.CampaignCode, &r.CampaignTitle, &r.DiscountRate); err != nil { @@ -214,6 +261,12 @@ ORDER BY mm.code, l.dim1, COALESCE(l.dim3, 0), sc.discount_rate DESC; if r.Dim3.Valid { d3 = r.Dim3.Int64 } + if r.Dim1 > 0 { + dimIDs = append(dimIDs, r.Dim1) + } + if d3 > 0 { + dimIDs = append(dimIDs, d3) + } mailRows = append(mailRows, wholesaleCampaignMailRow{ ProductCode: code, UrunIlkGrubu: group, @@ -236,6 +289,20 @@ ORDER BY mm.code, l.dim1, COALESCE(l.dim3, 0), sc.discount_rate DESC; return } + tokens := resolveDimTokens(ctx, dimIDs) + for i := range mailRows { + mailRows[i].Dim1Token = tokens[mailRows[i].Dim1] + if mailRows[i].Dim1Token == "" && mailRows[i].Dim1 > 0 { + mailRows[i].Dim1Token = fmt.Sprintf("%d", mailRows[i].Dim1) + } + if mailRows[i].Dim3 > 0 { + mailRows[i].Dim3Token = tokens[mailRows[i].Dim3] + if mailRows[i].Dim3Token == "" { + mailRows[i].Dim3Token = fmt.Sprintf("%d", mailRows[i].Dim3) + } + } + } + byGroup := map[string][]wholesaleCampaignMailRow{} for _, r := range mailRows { g := strings.TrimSpace(r.UrunIlkGrubu) diff --git a/svc/routes/wholesale_campaigns.go b/svc/routes/wholesale_campaigns.go index 44b3eab..9729d52 100644 --- a/svc/routes/wholesale_campaigns.go +++ b/svc/routes/wholesale_campaigns.go @@ -777,17 +777,22 @@ DO UPDATE SET dim_id = EXCLUDED.dim_id, updated_at = EXCLUDED.updated_at } d1 := int64(0) - // Resolve dim1: prefer ColorCode first (matches e-comm expectation: dim1=Color). - if id, ok := resolveDimID("dimval1", colorCode); ok { + // IMPORTANT: In this Nebim setup, both ItemDim1Code and ItemDim3Code are mapped in PG as dimval1 ids. + // 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", dim1Code); ok { + } else if id, ok := resolveDimID("dimval1", dim3Code); ok { + d1 = id + } else if id, ok := resolveDimID("dimval1", colorCode); ok { d1 = id } if d1 <= 0 { continue } d3k := int64(0) - if id, ok := resolveDimID("dimval3", t3); ok { + if id, ok := resolveDimID("dimval1", dim3Code); ok { + d3k = id + } else if id, ok := resolveDimID("dimval1", colorCode); ok { d3k = id }