package routes import ( "context" "database/sql" "fmt" "log" "sort" "strings" "time" "bssapp-backend/db" "bssapp-backend/internal/mailer" "bssapp-backend/models" "bssapp-backend/queries" "github.com/lib/pq" ) type wholesaleCampaignMailRow struct { ProductCode string UrunIlkGrubu string Marka string BrandGroupSec string Dim1 int64 Dim3 int64 Dim1Token string Dim3Token string CampaignCode string CampaignTitle string DiscountRate float64 } func buildWholesaleCampaignChangeMailHTML(firstGroupCode string, rows []wholesaleCampaignMailRow, actor string, at time.Time) string { var b strings.Builder b.WriteString(`
`) b.WriteString(`
`) b.WriteString(`
Kampanya Degisikligi
`) b.WriteString(`
Urun Ilk Grubu: ` + htmlEscapeMini(firstGroupCode) + `
`) if strings.TrimSpace(actor) != "" { b.WriteString(`
Islem Yapan: ` + htmlEscapeMini(actor) + `
`) } b.WriteString(`
Tarih: ` + htmlEscapeMini(at.Format("02.01.2006 15:04")) + `
`) b.WriteString(`
Varyant Sayisi: ` + fmt.Sprintf("%d", len(rows)) + `
`) b.WriteString(`
`) b.WriteString(`
`) b.WriteString(``) b.WriteString(``) heads := []string{ "MARKA GRUBU", "MARKA", "URUN KODU", "DIM1", "DIM3", "KAMPANYA", "IND %", } for _, h := range heads { b.WriteString(``) } b.WriteString(``) for _, r := range rows { b.WriteString(``) campaignLabel := strings.TrimSpace(r.CampaignCode) if t := strings.TrimSpace(r.CampaignTitle); t != "" { if campaignLabel != "" { campaignLabel = campaignLabel + " - " + t } else { campaignLabel = t } } cells := []string{ r.BrandGroupSec, r.Marka, r.ProductCode, strings.TrimSpace(r.Dim1Token), func() string { if strings.TrimSpace(r.Dim3Token) != "" { return strings.TrimSpace(r.Dim3Token) } return "" }(), campaignLabel, fmt.Sprintf("%.2f", r.DiscountRate), } for i, c := range cells { align := "left" if i == 3 || i == 4 || i == 6 { align = "right" } b.WriteString(``) } b.WriteString(``) } b.WriteString(`
` + htmlEscapeMini(h) + `
` + htmlEscapeMini(strings.TrimSpace(c)) + `
`) b.WriteString(`
Bu e-posta BSSApp sistemi tarafindan otomatik olusturulmustur.
`) b.WriteString(`
`) return b.String() } // sendWholesaleCampaignChangeMails sends one mail per UrunIlkGrubu using existing pricing mail mapping tables. // It lists only variants that currently have a campaign assigned. func sendWholesaleCampaignChangeMails(bg context.Context, ml *mailer.GraphMailer, productCodes []string, actor string) { if ml == nil { return } pg := db.PgDB if pg == nil { log.Printf("[campaign-mail] skipped: pg not ready") return } // Ensure mapping tables exist (reuse pricing mapping). if err := ensureFirstGroupMailMappingTables(pg); err != nil { log.Printf("[campaign-mail] mapping bootstrap error: %v", err) return } ctx, cancel := context.WithTimeout(bg, 90*time.Second) defer cancel() codes := make([]string, 0, len(productCodes)) seen := map[string]struct{}{} for _, c := range productCodes { c = strings.TrimSpace(c) if c == "" { continue } if _, ok := seen[c]; ok { continue } seen[c] = struct{}{} codes = append(codes, c) } if len(codes) == 0 { return } // Product info for grouping (UrunIlkGrubu, Marka, BrandGroupSec) comes from Nebim query. // This is best-effort: if MSSQL is down, we still send a single mail under group "UNKNOWN". productInfo := map[string]models.ProductPricing{} { rows, err := queries.GetAllProductPricingRows(ctx, 500, queries.ProductPricingFilters{ProductCode: codes}, "productCode", false) if err == nil { for _, r := range rows { code := strings.TrimSpace(r.ProductCode) if code != "" { productInfo[code] = r } } } } type dbRow struct { ProductCode string Dim1 int64 Dim3 sql.NullInt64 CampaignCode string CampaignTitle string DiscountRate float64 } rows, err := pg.QueryContext(ctx, ` WITH mm AS ( SELECT id AS mmitem_id, code FROM mmitem WHERE code = ANY($1::text[]) ), latest AS ( SELECT DISTINCT ON (z.mmitem_id, z.dim1, COALESCE(z.dim3, 0)) z.mmitem_id, z.dim1, z.dim3, z.sdcampaign_id FROM zbggcampaign z JOIN mm ON mm.mmitem_id = z.mmitem_id ORDER BY z.mmitem_id, z.dim1, COALESCE(z.dim3, 0), z.id DESC ) SELECT mm.code AS product_code, l.dim1, l.dim3, COALESCE(sc.code,'') AS campaign_code, COALESCE(sc.title,'') AS campaign_title, COALESCE(sc.discount_rate, 0)::float8 AS discount_rate FROM mm JOIN latest l ON l.mmitem_id = mm.mmitem_id JOIN sdcampaign sc ON sc.id = l.sdcampaign_id WHERE COALESCE(sc.is_active, TRUE) = TRUE ORDER BY mm.code, l.dim1, COALESCE(l.dim3, 0), sc.discount_rate DESC; `, pq.Array(codes)) if err != nil { log.Printf("[campaign-mail] campaign rows query error: %v", err) return } 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 { log.Printf("[campaign-mail] scan error: %v", err) return } code := strings.TrimSpace(r.ProductCode) info := productInfo[code] group := strings.TrimSpace(info.UrunIlkGrubu) if group == "" { group = "UNKNOWN" } d3 := int64(0) 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, Marka: strings.TrimSpace(info.Marka), BrandGroupSec: strings.TrimSpace(info.BrandGroupSec), Dim1: r.Dim1, Dim3: d3, CampaignCode: strings.TrimSpace(r.CampaignCode), CampaignTitle: strings.TrimSpace(r.CampaignTitle), DiscountRate: r.DiscountRate, }) } if err := rows.Err(); err != nil { log.Printf("[campaign-mail] rows error: %v", err) return } if len(mailRows) == 0 { // Nothing assigned => no mail. 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) if g == "" { g = "UNKNOWN" } byGroup[g] = append(byGroup[g], r) } now := time.Now() for group, list := range byGroup { recipients, err := loadPricingRecipients(pg, group) if err != nil { log.Printf("[campaign-mail] recipient query error group=%s err=%v", group, err) continue } if len(recipients) == 0 { log.Printf("[campaign-mail] no recipients mapped group=%s", group) continue } sort.Slice(list, func(i, j int) bool { if list[i].ProductCode != list[j].ProductCode { return list[i].ProductCode < list[j].ProductCode } if list[i].Dim1 != list[j].Dim1 { return list[i].Dim1 < list[j].Dim1 } return list[i].Dim3 < list[j].Dim3 }) subject := fmt.Sprintf("Kampanya Degisikligi | %s | %s | %d varyant", group, now.Format("02.01.2006 15:04"), len(list)) html := buildWholesaleCampaignChangeMailHTML(group, list, actor, now) stepCtx, stepCancel := context.WithTimeout(bg, 25*time.Second) err = ml.Send(stepCtx, mailer.Message{ To: recipients, Subject: subject, BodyHTML: html, }) stepCancel() if err != nil { log.Printf("[campaign-mail] send failed group=%s err=%v", group, err) } else { log.Printf("[campaign-mail] sent group=%s to=%d variants=%d", group, len(recipients), len(list)) } } }